From: Alex Kamedov Date: Wed, 27 Apr 2011 14:49:43 +0000 (+0600) Subject: Merge branch 'develop' X-Git-Tag: 22.4~32^2~19 X-Git-Url: https://git.mdrn.pl/django-cas-provider.git/commitdiff_plain/fdd4b59a0a825cf8fa38e94123d118b57707b067?hp=73bfc6b53448f5f1d10d9b3f58f2cf4f3756be41 Merge branch 'develop' --- diff --git a/AUTHORS.txt b/AUTHORS.txt new file mode 100644 index 0000000..af9d445 --- /dev/null +++ b/AUTHORS.txt @@ -0,0 +1,5 @@ +Chris Williams +Alex Kamedov +Łukasz Rekucki +Marek Stepniowski +Fred Wenzel diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..775b4b5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include cas_provider/templates *.html +include AUTHORS.txt +include README.rst +include LICENSE \ No newline at end of file diff --git a/README.rst b/README.rst index bc2874d..0ef1742 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,6 @@ django-cas-provider =================== ---------------------------------- -Chris Williams ---------------------------------- - OVERVIEW ========= @@ -26,7 +22,9 @@ To install, run the following command from this directory: ``python setup.py install`` Or, put cas_provider somewhere on your Python path. - + +If you want use CAS v.2 protocol or above, you must install `lxml` package to correct work. + USAGE ====== @@ -39,8 +37,16 @@ USAGE SETTINGS ========= -CAS_TICKET_EXPIRATION - minutes to tickets expiration (default is 5 minutes) -CAS_CHECK_SERVICE - check if ticket service is equal with service GET argument +CAS_TICKET_EXPIRATION - minutes to tickets expiration. Default is 5 minutes. + +CAS_CUSTOM_ATTRIBUTES_CALLBACK - name of callback to provide dictionary with +extended user attributes (may be used in CAS v.2 or above). Default is None. + +CAS_CUSTOM_ATTRIBUTES_FORMAT - name of custom attribute formatter callback will be +used to format custom user attributes. This package provide module `attribute_formatters` +with formatters for common used formats. Available formats styles are `RubyCAS`, `Jasig` +and `Name-Value. Default is Jasig style. See module source code for more details. + PROTOCOL DOCUMENTATION ===================== @@ -68,8 +74,8 @@ Optional arguments: * warn_template_name - warning page template name to allow login user to service if he already authenticated in SSO (default is 'cas/warn.html') -If request.GET has 'warn' argument - it shows warning message if user has already -authenticated in SSO instead of generate Service Ticket and redirect. +If request.GET has 'warn' argument and user has already authenticated in SSO it shows +warning message instead of generate Service Ticket and redirect. logout ----------- @@ -101,3 +107,57 @@ Work with proxy is not supported yet. It has not arguments. +CUSTOM USER ATTRIBUTES FORMAT +=========================== + +Custom attribute format style may be changed in project settings with +CAS_CUSTOM_ATTRIBUTES_FORMAT constant. You can provide your own formatter callback +or specify existing in this package in `attribute_formatters` module. + +Attribute formatter callback takes two arguments: + +* `auth_success` - `cas:authenticationSuccess` node. It is `lxml.etree.SubElement`instance; +* `attrs` - dictionary with user attributes received from callback specified in + CAS_CUSTOM_ATTRIBUTES_CALLBACK in project settings. + +Example of generated XML below: + + + + jsmith + + + + PGTIOU-84678-8a9d2sfa23casd + + + + +* Name-Value style (provided in `cas_provider.attribute_formatters.name_value`): + + + + + + + + +* Jasig Style attributes (provided in `cas_provider.attribute_formatters.jasig`): + + + Jasig + Smith + John + CN=Staff,OU=Groups,DC=example,DC=edu + CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu + + + +* RubyCAS style (provided in `cas_provider.attribute_formatters.ruby_cas`): + + RubyCAS + Smith + John + CN=Staff,OU=Groups,DC=example,DC=edu + CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu + diff --git a/cas_provider/__init__.py b/cas_provider/__init__.py index 91b3b2c..449c7b6 100644 --- a/cas_provider/__init__.py +++ b/cas_provider/__init__.py @@ -4,7 +4,8 @@ __all__ = [] _DEFAULTS = { 'CAS_TICKET_EXPIRATION': 5, # In minutes - 'CAS_CHECK_SERVICE': False, + 'CAS_CUSTOM_ATTRIBUTES_CALLBACK': None, + 'CAS_CUSTOM_ATTRIBUTES_FORMATER': 'cas_provider.attribute_formatters.jasig', } for key, value in _DEFAULTS.iteritems(): diff --git a/cas_provider/attribute_formatters.py b/cas_provider/attribute_formatters.py new file mode 100644 index 0000000..3f3ab3c --- /dev/null +++ b/cas_provider/attribute_formatters.py @@ -0,0 +1,41 @@ +from lxml import etree + +CAS_URI = 'http://www.yale.edu/tp/cas' +NSMAP = {'cas': CAS_URI} +CAS = '{%s}' % CAS_URI + + +def jasig(auth_success, attrs): + attributes = etree.SubElement(auth_success, CAS + 'attributes') + style = etree.SubElement(attributes, CAS + 'attraStyle') + style.text = u'Jasig' + for name, value in attrs.items(): + if isinstance(value, list): + for e in value: + element = etree.SubElement(attributes, CAS + name) + element.text = e + else: + element = etree.SubElement(attributes, CAS + name) + element.text = value + + +def ruby_cas(auth_success, attrs): + style = etree.SubElement(auth_success, CAS + 'attraStyle') + style.text = u'RubyCAS' + for name, value in attrs.items(): + if isinstance(value, list): + for e in value: + element = etree.SubElement(auth_success, CAS + name) + element.text = e + else: + element = etree.SubElement(auth_success, CAS + name) + element.text = value + +def name_value(auth_success, attrs): + etree.SubElement(auth_success, CAS + 'attribute', name=u'attraStyle', value=u'Name-Value') + for name, value in attrs.items(): + if isinstance(value, list): + for e in value: + etree.SubElement(auth_success, CAS + 'attribute', name=name, value=e) + else: + etree.SubElement(auth_success, CAS + 'attribute', name=name, value=value) diff --git a/cas_provider/templates/cas/login.html b/cas_provider/templates/cas/login.html index d61974a..d9e933e 100644 --- a/cas_provider/templates/cas/login.html +++ b/cas_provider/templates/cas/login.html @@ -5,9 +5,10 @@ Login {% endblock %} {% block content %} -
+
Log in to your account + {% csrf_token %} {% if errors %}
    {% for error in errors %} diff --git a/cas_provider/templates/cas/warn.html b/cas_provider/templates/cas/warn.html index 58f4ed8..ca9e561 100644 --- a/cas_provider/templates/cas/warn.html +++ b/cas_provider/templates/cas/warn.html @@ -5,7 +5,7 @@ Warning {% endblock %} {% block content %} - +
    Confirm to log in to {{ service }} diff --git a/cas_provider/tests.py b/cas_provider/tests.py index c876148..4b486de 100644 --- a/cas_provider/tests.py +++ b/cas_provider/tests.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test import TestCase from urlparse import urlparse +from django.conf import settings class ViewsTest(TestCase): @@ -35,12 +36,16 @@ class ViewsTest(TestCase): self.assertTemplateUsed(response, 'cas/warn.html') + def _cas_logout(self): + response = self.client.get(reverse('cas_logout'), follow=False) + self.assertEqual(response.status_code, 200) + + def test_logout(self): response = self._login_user('root', '123') self._validate_cas1(response, True) - response = self.client.get(reverse('cas_logout'), follow=False) - self.assertEqual(response.status_code, 200) + self._cas_logout() response = self.client.get(reverse('cas_login'), follow=False) self.assertEqual(response.status_code, 200) @@ -58,7 +63,58 @@ class ViewsTest(TestCase): def test_cas2_success_validate(self): response = self._login_user('root', '123') - self._validate_cas2(response, True) + response = self._validate_cas2(response, True) + user = User.objects.get(username=self.username) + self.assertEqual(response.content, _cas2_sucess_response(user).content) + + def test_cas2_custom_attrs(self): + settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK = cas_mapping + response = self._login_user('editor', '123') + + response = self._validate_cas2(response, True) + self.assertEqual(response.content, '''''' + '''''' + '''editor''' + '''''' + '''Jasig''' + '''editor''' + '''True''' + '''True''' + '''editor@exapmle.com''' + '''''' + '''''' + '''''') + + self._cas_logout() + response = self._login_user('editor', '123') + settings.CAS_CUSTOM_ATTRIBUTES_FORMATER = 'cas_provider.attribute_formatters.ruby_cas' + response = self._validate_cas2(response, True) + self.assertEqual(response.content, '''''' + '''''' + '''editor''' + '''RubyCAS''' + '''editor''' + '''True''' + '''True''' + '''editor@exapmle.com''' + '''''' + '''''') + + self._cas_logout() + response = self._login_user('editor', '123') + settings.CAS_CUSTOM_ATTRIBUTES_FORMATER = 'cas_provider.attribute_formatters.name_value' + response = self._validate_cas2(response, True) + self.assertEqual(response.content, '''''' + '''''' + '''editor''' + '''''' + '''''' + '''''' + '''''' + '''''' + '''''' + '''''') + def test_cas2_fail_validate(self): for user, pwd in (('root', '321'), ('notroot', '123'), ('nonactive', '123')): @@ -120,7 +176,6 @@ class ViewsTest(TestCase): response = self.client.get(reverse('cas_service_validate'), {'ticket': ticket, 'service': self.service}, follow=False) self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, _cas2_sucess_response(self.username).content) else: self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context['form'].errors), 1) @@ -128,6 +183,7 @@ class ViewsTest(TestCase): response = self.client.get(reverse('cas_service_validate'), {'ticket': 'ST-12312312312312312312312', 'service': self.service}, follow=False) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, _cas2_error_response(INVALID_TICKET).content) + return response class ModelsTestCase(TestCase): @@ -141,3 +197,11 @@ class ModelsTestCase(TestCase): ticket = ServiceTicket.objects.create(service='http://example.com', user=self.user) self.assertEqual(ticket.get_redirect_url(), '%(service)s?ticket=%(ticket)s' % ticket.__dict__) + +def cas_mapping(user): + return { + 'is_staff': unicode(user.is_staff), + 'is_active': unicode(user.is_active), + 'email': user.email, + 'group': [g.name for g in user.groups.all()] + } diff --git a/cas_provider/views.py b/cas_provider/views.py index 32ed6e9..4a7e9b8 100644 --- a/cas_provider/views.py +++ b/cas_provider/views.py @@ -1,6 +1,6 @@ from django.conf import settings -from django.contrib.auth import login as auth_login, \ - logout as auth_logout +from django.contrib.auth import login as auth_login, logout as auth_logout +from django.core.urlresolvers import get_callable from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render_to_response from django.template import RequestContext @@ -70,6 +70,7 @@ def validate(request): # TODO: check user SSO session try: ticket = ServiceTicket.objects.get(ticket=ticket_string) + assert ticket.service == service username = ticket.user.username ticket.delete() return HttpResponse("yes\n%s\n" % username) @@ -97,13 +98,13 @@ def service_validate(request): except ServiceTicket.DoesNotExist: return _cas2_error_response(INVALID_TICKET) - if settings.CAS_CHECK_SERVICE and ticket.service != service: + if ticket.service != service: ticket.delete() return _cas2_error_response(INVALID_SERVICE) - username = ticket.user.username + user = ticket.user ticket.delete() - return _cas2_sucess_response(username) + return _cas2_sucess_response(user) def _cas2_error_response(code): @@ -117,9 +118,22 @@ def _cas2_error_response(code): }, mimetype='text/xml') -def _cas2_sucess_response(username): - return HttpResponse(u''' - - %(username)s - - ''' % {'username': username}, mimetype='text/xml') +def _cas2_sucess_response(user): + return HttpResponse(auth_success_response(user), mimetype='text/xml') + + +def auth_success_response(user): + from attribute_formatters import CAS, NSMAP, etree + + response = etree.Element(CAS + 'serviceResponse', nsmap=NSMAP) + auth_success = etree.SubElement(response, CAS + 'authenticationSuccess') + username = etree.SubElement(auth_success, CAS + 'user') + username.text = user.username + + if settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK: + callback = get_callable(settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK) + attrs = callback(user) + if len(attrs) > 0: + formater = get_callable(settings.CAS_CUSTOM_ATTRIBUTES_FORMATER) + formater(auth_success, attrs) + return unicode(etree.tostring(response, encoding='utf-8'), 'utf-8') diff --git a/setup.py b/setup.py index 8f06deb..2d65aaf 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ - from setuptools import setup, find_packages - + + setup( name='django-cas-provider', - version='0.1dev', + version='0.2.2', description='A "provider" for the Central Authentication Service (http://jasig.org/cas)', author='Chris Williams', author_email='chris@nitron.org',