Implement validation of service through setting, store Django session_key in tickets, fix implementation of logout
This commit is contained in:
parent
d097509b3f
commit
0922e03caf
11
README.txt
11
README.txt
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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)))
|
||||
|
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
Reference in New Issue