Implement validation of service through setting, store Django session_key in tickets, fix implementation of logout

This commit is contained in:
Benjamin Dauvergne 2014-10-28 18:40:52 +01:00
parent d097509b3f
commit 0922e03caf
11 changed files with 195 additions and 49 deletions

View File

@ -9,7 +9,12 @@ be automatically loaded by the plugin framework.
Settings
========
*TODO*
Name Description
================= =========================
A2_IDP_CAS_PROVIDER
A2_IDP_CAS_TICKET_EXPIRATION
A2_IDP_CAS_SERVICES A sequence of URL prefixes, any URL starting with this
prefix is authorized to request a ticket
A2_IDP_CAS_PROVIDER Class implementating CAS views, default to
`authentic2_idp_cas.views.CasProvider`
A2_IDP_CAS_TICKET_EXPIRATION Ticket lifetime

View File

@ -1,6 +1,7 @@
from django.utils.timezone import now
from django.template.loader import render_to_string
from . import utils
__version__ = '1.0'
class Plugin(object):
@ -12,22 +13,15 @@ class Plugin(object):
return [__name__]
def logout_list(self, request):
from . import models
qs = models.CasService.objects.filter(accesstoken__user=request.user,
accesstoken__expires__gt=now(), logout_url__isnull=False) \
.distinct()
l = []
for client in qs:
name = client.name
url = client.get_logout_url()
fragments = []
for name, logout in utils.get_logout_urls(request):
url = logout.get_logout_url()
ctx = {
'needs_iframe': client.logout_use_iframe,
'needs_iframe': logout.logout_use_iframe,
'name': name,
'url': url,
'iframe_timeout': client.logout_use_iframe_timeout,
'iframe_timeout': logout.logout_use_iframe_timeout,
}
content = render_to_string('idp/saml/logout_fragment.html', ctx)
l.append(content)
return l
content = render_to_string('authentic2_idp_cas/logout_fragment.html', ctx)
fragments.append(content)
return fragments

View File

@ -1,28 +1,35 @@
from django.utils.importlib import import_module
class AppSettings(object):
__DEFAULTS = {
'SERVICES': (),
}
def __init__(self, prefix):
self.prefix = prefix
@property
def PROVIDER(self):
from django.utils.importlib import import_module
cas_provider = self._setting('CAS_PROVIDER', 'authentic2_idp_cas.views.Authentic2CasProvider')
module, cls = cas_provider.rsplit('.', 1)
module = import_module(module)
return getattr(module, cls)
@property
def TICKET_EXPIRATION(self):
return self._setting('TICKET_EXPIRATION', 240)
def _setting(self, name, dflt):
from django.conf import settings
return getattr(settings, self.prefix + name, dflt)
def __getattr__(self, name):
if name not in self.__DEFAULTS:
raise AttributeError(name)
return self._setting(name, self.__DEFAULTS[name])
# Ugly? Guido recommends this himself ...
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html

View File

@ -19,4 +19,15 @@ class CasTicketQuerySet(query.QuerySet):
qs = self.filter(creation__lt=now()-delta)
qs.delete()
class CasServiceQuerySet(query.QuerySet):
def for_domain(self, domain):
q = query.Q(domain=domain)
parts = domain.split('.')
for i in range(1, len(parts)):
q |= query.Q(domain='.%s' % '.'.join(parts[i:]))
return self.filter(q).order_by('-domain')
CasServiceManager = managers.PassThroughManager.for_queryset_class(CasServiceQuerySet)
CasTicketManager = managers.PassThroughManager.for_queryset_class(CasTicketQuerySet)

View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CasTicket'
db.create_table(u'authentic2_idp_cas_casticket', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('ticket_id', self.gf('django.db.models.fields.CharField')(max_length=64)),
('renew', self.gf('django.db.models.fields.BooleanField')(default=False)),
('validity', self.gf('django.db.models.fields.BooleanField')(default=False)),
('service', self.gf('django.db.models.fields.CharField')(max_length=256)),
('user', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
('creation', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('expire', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
('session_key', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=64, blank=True)),
))
db.send_create_signal(u'authentic2_idp_cas', ['CasTicket'])
# Adding model 'CasService'
db.create_table(u'authentic2_idp_cas_casservice', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('logout_url', self.gf('django.db.models.fields.URLField')(max_length=255, null=True, blank=True)),
('logout_use_iframe', self.gf('django.db.models.fields.BooleanField')(default=False)),
('logout_use_iframe_timeout', self.gf('django.db.models.fields.PositiveIntegerField')(default=300)),
('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=128)),
('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=128)),
('domain', self.gf('django.db.models.fields.CharField')(unique=True, max_length=128)),
))
db.send_create_signal(u'authentic2_idp_cas', ['CasService'])
def backwards(self, orm):
# Deleting model 'CasTicket'
db.delete_table(u'authentic2_idp_cas_casticket')
# Deleting model 'CasService'
db.delete_table(u'authentic2_idp_cas_casservice')
models = {
u'authentic2_idp_cas.casservice': {
'Meta': {'object_name': 'CasService'},
'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'logout_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'logout_use_iframe': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'logout_use_iframe_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '300'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128'})
},
u'authentic2_idp_cas.casticket': {
'Meta': {'object_name': 'CasTicket'},
'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'expire': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'renew': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'service': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
'session_key': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
'ticket_id': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'user': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
'validity': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
}
}
complete_apps = ['authentic2_idp_cas']

View File

@ -1,11 +1,10 @@
from datetime import timedelta
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import now
from authentic2.models import LogoutUrlAbstract
from . import app_setting, managers
from . import managers
class CasTicket(models.Model):
@ -15,10 +14,11 @@ class CasTicket(models.Model):
renew = models.BooleanField(default=False)
validity = models.BooleanField(default=False)
service = models.CharField(max_length=256)
user = models.CharField(max_length=128,blank=True,null=True)
user = models.CharField(max_length=128, blank=True, null=True)
creation = models.DateTimeField(auto_now_add=True)
'''Duration length for the ticket as seconds'''
expire = models.DateTimeField(blank=True, null=True)
session_key = models.CharField(max_length=64, db_index=True, blank=True)
objects = managers.CasTicketManager()
@ -38,5 +38,9 @@ class CasService(LogoutUrlAbstract):
domain = models.CharField(max_length=128, unique=True,
verbose_name=_('domain'))
class Meta:
objects = managers.CasServiceManager()
class Meta:
verbose_name = _('cas service')
verbose_name_plural = _('cas services')

View File

@ -0,0 +1,10 @@
{% load i18n %}
<div>{% blocktrans %}Sending logout to {{ name }}....{% endblocktrans %}
{% if needs_iframe %}
<iframe src="{{ url }}" marginwidth="0" marginheight="0" scrolling="no" style="border: none"
width="16" height="16" onload="setTimeout(function () { window.iframe_count -= 1; }, {{ iframe_timeout }})">
</iframe>
{% else %}
<img src="{{ url }}" width="16" height="16">
{% endif %}
</div>

View File

@ -3,4 +3,4 @@ from django.conf.urls import patterns, include
from . import app_settings
urlpatterns = patterns('',
('^idp/cas/', include(app_settings.PROVIDER()().url)))
('^idp/cas/', include(app_settings.PROVIDER().url)))

View File

@ -0,0 +1,37 @@
import random
import string
import urlparse
import urllib
ALPHABET = string.letters+string.digits+'-'
def get_logout_urls(request):
'''Retrieve logout urls'''
return ()
def make_id(prefix='', length=29):
'''Generate CAS tickets identifiers'''
l = length-len(prefix)
content = ( random.SystemRandom().choice(ALPHABET) for x in range(l) )
return prefix + ''.join(content)
def url_overwrite_parameters(url, **kwargs):
splitted_url = urlparse.urlsplit(url)
query = splitted_url.query
parsed_query = urlparse.parse_qsl(query)
parsed_query = [(a,b) for a, b in parsed_query if a not in kwargs]
for a, b in kwargs.iteritems():
parsed_query.append((a, unicode(b).encode('utf-8')))
query = urllib.urlencode(parsed_query)
splitted_url = splitted_url[:3] + (query,) + splitted_url[4:]
return urlparse.urlunsplit(splitted_url)
def url_add_parameters(url, **kwargs):
splitted_url = urlparse.urlsplit(url)
query = splitted_url.query
parsed_query = urlparse.parse_qsl(query)
for a, b in kwargs.iteritems():
parsed_query.append((a, unicode(b).encode('utf-8')))
query = urllib.urlencode(parsed_query)
splitted_url = splitted_url[:3] + (query,) + splitted_url[4:]
return urlparse.urlunsplit(splitted_url)

View File

@ -1,8 +1,6 @@
import urlparse
import logging
import random
import datetime
import string
from xml.etree import ElementTree as ET
from django.http import HttpResponseRedirect, HttpResponseBadRequest, \
@ -21,15 +19,13 @@ from constants import SERVICE_PARAM, RENEW_PARAM, GATEWAY_PARAM, ID_PARAM, \
CANCEL_PARAM, SERVICE_TICKET_PREFIX, TICKET_PARAM, \
CAS10_VALIDATION_FAILURE, CAS10_VALIDATION_SUCCESS, PGT_URL_PARAM, \
INVALID_REQUEST_ERROR, INVALID_TICKET_ERROR, INVALID_SERVICE_ERROR, \
INTERNAL_ERROR, CAS20_VALIDATION_FAILURE, CAS20_VALIDATION_SUCCESS, \
INTERNAL_ERROR, CAS20_VALIDATION_FAILURE, \
CAS_NAMESPACE, USER_ELT, SERVICE_RESPONSE_ELT, AUTHENTICATION_SUCCESS_ELT
from . import models
from . import models, utils, app_settings
logger = logging.getLogger(__name__)
ALPHABET = string.letters+string.digits+'-'
SAML_RESPONSE_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
@ -79,21 +75,18 @@ class CasProvider(object):
url('^logout/$', self.logout))
url = property(get_url)
def make_id(self, prefix='', length=29):
l = length-len(prefix)
content = ( random.SystemRandom().choice(ALPHABET) for x in range(l) )
return prefix + ''.join(content)
def create_service_ticket(self, service, renew=False, validity=True,
expire=None, user=None):
expire=None, user=None, session_key=''):
'''Create a fresh service ticket'''
validity = validity and not renew
return CasTicket.objects.create(ticket_id=self.make_id(prefix='ST-'),
return CasTicket.objects.create(
ticket_id=utils.make_id(prefix='ST-'),
service=service,
renew=renew,
validity=validity,
expire=None,
user=user)
user=user,
session_key=session_key)
def check_authentication(self, request, st):
'''
@ -120,11 +113,17 @@ class CasProvider(object):
if not service.startswith('http://') and not \
service.startswith('https://'):
return self.failure(request, 'service is not an HTTP or HTTPS URL')
scheme, domain, x, x, x, x = urlparse.urlparse(service)
try:
cas_service = models.CasService.get(domain=domain)
except models.CasService.DoesNotExist:
self.failure(request, 'service %r is not allowed' % service)
# verify service is authorized !
for allowed_service in app_settings.SERVICES:
if service.startswith(allowed_service):
break
else:
scheme, domain, x, x, x, x = urlparse.urlparse(service)
try:
cas_service = models.CasService.get(domain=domain)
except models.CasService.DoesNotExist:
self.failure(request, 'service %r is not allowed' % service)
return self.handle_login(request, cas_service, service, renew, gateway)
def must_authenticate(self, request, renew):
@ -171,7 +170,8 @@ renew:%s and gateway:%s' % (service, renew, gateway))
return self.authenticate(request, st, passive=gateway)
else:
st = self.create_service_ticket(service, expire=expire,
user=self.get_cas_user(request))
user=self.get_cas_user(request),
session_key=request.session.session_key)
return self.handle_login_after_authentication(request, st)
def cas_failure(self, request, st, reason):
@ -228,6 +228,7 @@ renew:%s and gateway:%s' % (service, renew, gateway))
# normal login
st.user = self.get_cas_user(request)
st.validity = True
st.session_key = request.session.session_key
st.save()
return self.handle_login_after_authentication(request, st)
@ -385,7 +386,12 @@ renew:%s and gateway:%s' % (service, renew, gateway))
return self.cas20_error(INTERNAL_ERROR)
def logout(self, request):
return HttpResponseRedirect(settings.LOGOUT_URL)
url = request.REQUEST.get('url')
logout_url = settings.LOGOUT_URL
if url:
logout_url = utils.url_add_parameters(logout_url,
next=url)
return HttpResponseRedirect(logout_url)
class Authentic2CasProvider(CasProvider):
def authenticate(self, request, st, passive=False):
@ -408,6 +414,7 @@ is possible''')
ae = AuthenticationEvent.objects.get(nonce=st.ticket_id)
st.user = ae.who
st.validity = True
st.session_key = request.session.session_key
st.save()
return True
except AuthenticationEvent.DoesNotExist: