From: deyk Date: Fri, 13 Apr 2012 22:25:48 +0000 (-0700) Subject: Merge remote-tracking branch 'cas2/master' X-Git-Tag: 22.4~32 X-Git-Url: https://git.mdrn.pl/django-cas-provider.git/commitdiff_plain/82555c55f77395067231c1c22685597f12706902?hp=8ace0586471d741bbe1d55dca48f524a3d385852 Merge remote-tracking branch 'cas2/master' Conflicts: .gitignore cas_provider/forms.py cas_provider/models.py cas_provider/urls.py cas_provider/views.py Merged from https://github.com/castlabs/django-cas-provider PT #27996721 --- diff --git a/.gitignore b/.gitignore index 9063127..4030171 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ +# Python garbage *.pyc -django_cas_provider.egg-info/ +*.egg-info + +# Mac OS X garbage +.DS_Store + +# PyDev garbage +.tmp* diff --git a/AUTHORS.txt b/AUTHORS.txt new file mode 100644 index 0000000..9900ace --- /dev/null +++ b/AUTHORS.txt @@ -0,0 +1,6 @@ +Chris Williams +Alex Kamedov +Łukasz Rekucki +Marek Stepniowski +Fred Wenzel +Sebastian Annies diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a87457c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +recursive-include cas_provider/templates *.html +recursive-include cas_provider/fixtures * +include AUTHORS.txt +include README.rst +include LICENSE \ No newline at end of file diff --git a/README.rst b/README.rst index 1d32df1..258d685 100644 --- a/README.rst +++ b/README.rst @@ -2,31 +2,46 @@ django-cas-provider =================== ---------------------------------- -Chris Williams ---------------------------------- - OVERVIEW ========= -django-cas-provider is a provider for the `Central Authentication -Service `_. It supports CAS version 1.0. It allows -remote services to authenticate users for the purposes of -Single Sign-On (SSO). For example, a user logs into a CAS server -(provided by django-cas-provider) and can then access other services -(such as email, calendar, etc) without re-entering her password for -each service. For more details, see the `CAS wiki `_ -and `Single Sign-On on Wikipedia `_. +django-cas-provider is a provider for the `Central Authentication Service `_. It supports CAS version 1.0 and parts of CAS version 2.0 protocol. It allows remote services to authenticate users for the purposes of Single Sign-On (SSO). For example, a user logs into a CAS server +(provided by django-cas-provider) and can then access other services (such as email, calendar, etc) without re-entering her password for each service. For more details, see the `CAS wiki `_ and `Single Sign-On on Wikipedia `_. INSTALLATION ============= -To install, run the following command from this directory: +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. + +UPDATING FROM PREVIOUS VERSION +=============================== + +I introduced south for DB schema migration. The schema from any previous version without south is 0001_initial. +You will get an error: + + ``Running migrations for cas_provider:`` + + ``- Migrating forwards to 0001_initial.`` + + ``> cas_provider:0001_initial`` + + ``Traceback (most recent call last):`` + + ``...`` + + ``django.db.utils.DatabaseError: relation "cas_provider_serviceticket" already exists`` + +to circumvent that problem you will need to fake the initial migration: + + python manage.py migrate cas_provider 0001_initial --fake - ``python setup.py install`` -Or, put cas_provider somewhere on your Python path. - USAGE ====== @@ -34,3 +49,136 @@ USAGE #. In *settings.py*, set ``LOGIN_URL`` to ``'/cas/login/'`` and ``LOGOUT_URL`` to ``'/cas/logout/'`` #. In *urls.py*, put the following line: ``(r'^cas/', include('cas_provider.urls')),`` #. Create login/logout templates (or modify the samples) +#. Use 'cleanuptickets' management command to clean up expired tickets + +SETTINGS +========= + +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. + +CAS_AUTO_REDIRECT_AFTER_LOGOUT - If False (default behavior, specified in CAS protocol) +after successful logout notification page will be shown. If it's True, after successful logout will +be auto redirect back to service without any notification. + + +PROTOCOL DOCUMENTATION +===================== + +* `CAS Protocol ` +* `CAS 1 Architecture ` +* `CAS 2 Architecture ` +* `Proxy Authentication ` +* `CAS – Central Authentication Service ` +* `Proxy CAS Walkthrough ` + +PROVIDED VIEWS +============= + +login +--------- + +It has not required arguments. + +Optional arguments: + +* template_name - login form template name (default is 'cas/login.html') +* success_redirect - redirect after successful login if service GET argument is not provided + (default is settings.LOGIN_REDIRECT_URL) +* 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 and user has already authenticated in SSO it shows +warning message instead of generate Service Ticket and redirect. + +logout +----------- + +This destroys a client's single sign-on CAS session. The ticket-granting cookie is destroyed, +and subsequent requests to login view will not obtain service tickets until the user again +presents primary credentials (and thereby establishes a new single sign-on session). + +It has not required arguments. + +Optional arguments: + +* template_name - template name for page with successful logout message (default is 'cas/logout.html') + +validate +------------- + +It checks the validity of a service ticket. It is part of the CAS 1.0 protocol and thus does +not handle proxy authentication. + +It has not arguments. + +service_validate +------------------------- + +It checks the validity of a service ticket and returns an XML-fragment response via CAS 2.0 protocol. +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 1a719b4..d5a3520 100644 --- a/cas_provider/__init__.py +++ b/cas_provider/__init__.py @@ -4,6 +4,9 @@ __all__ = [] _DEFAULTS = { 'CAS_TICKET_EXPIRATION': 5, # In minutes + 'CAS_CUSTOM_ATTRIBUTES_CALLBACK': None, + 'CAS_CUSTOM_ATTRIBUTES_FORMATER': 'cas_provider.attribute_formatters.jasig', + 'CAS_AUTO_REDIRECT_AFTER_LOGOUT': False, } for key, value in _DEFAULTS.iteritems(): @@ -12,4 +15,4 @@ for key, value in _DEFAULTS.iteritems(): except AttributeError: setattr(settings, key, value) except ImportError: - pass \ No newline at end of file + pass diff --git a/cas_provider/admin.py b/cas_provider/admin.py index 5091f8e..499fa43 100644 --- a/cas_provider/admin.py +++ b/cas_provider/admin.py @@ -1,11 +1,29 @@ from django.contrib import admin +from models import * -from models import ServiceTicket, LoginTicket class ServiceTicketAdmin(admin.ModelAdmin): - pass -admin.site.register(ServiceTicket, ServiceTicketAdmin) + list_display = ('user', 'service', 'created') + list_filter = ('created',) + +class ProxyTicketAdmin(admin.ModelAdmin): + list_display = ('user', 'service', 'created') + list_filter = ('created',) class LoginTicketAdmin(admin.ModelAdmin): - pass -admin.site.register(LoginTicket, LoginTicketAdmin) \ No newline at end of file + list_display = ('ticket', 'created') + list_filter = ('created',) + +class ProxyGrantingTicketAdmin(admin.ModelAdmin): + list_display = ('ticket', 'created') + list_filter = ('created',) + +class ProxyGrantingTicketIOUAdmin(admin.ModelAdmin): + list_display = ('ticket', 'created') + list_filter = ('created',) + +admin.site.register(ServiceTicket, ServiceTicketAdmin) +admin.site.register(ProxyTicket, ProxyTicketAdmin) +admin.site.register(LoginTicket, LoginTicketAdmin) +admin.site.register(ProxyGrantingTicket, ProxyGrantingTicketAdmin) +admin.site.register(ProxyGrantingTicketIOU, ProxyGrantingTicketIOUAdmin) 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/fixtures/cas_users.json b/cas_provider/fixtures/cas_users.json new file mode 100644 index 0000000..f31d5cb --- /dev/null +++ b/cas_provider/fixtures/cas_users.json @@ -0,0 +1,130 @@ +[ + { + "pk": 1, + "model": "auth.group", + "fields": { + "name": "editor", + "permissions": [] + } + }, + { + "pk": 2, + "model": "auth.group", + "fields": { + "name": "author", + "permissions": [] + } + }, + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "root", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2011-04-24 11:29:11", + "groups": [], + "user_permissions": [], + "password": "sha1$602c5$ba8608296f6bfcb352e978084b337a90d586ecc3", + "email": "root@example.com", + "date_joined": "2010-07-04 13:33:14" + } + }, + { + "pk": 26, + "model": "auth.user", + "fields": { + "username": "active", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2011-04-01 12:42:53", + "groups": [], + "user_permissions": [], + "password": "sha1$7dfb4$d19f8340a01b597089dfde6dc17bc5288c1f863e", + "email": "active@example.com", + "date_joined": "2011-04-01 11:12:45" + } + }, + { + "pk": 30, + "model": "auth.user", + "fields": { + "username": "author", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "last_login": "2011-04-24 11:32:16", + "groups": [ + 2 + ], + "user_permissions": [], + "password": "sha1$6c580$01509bea19e3ade9f1bcf303205a7cb10ce6762d", + "email": "", + "date_joined": "2011-04-24 11:32:16" + } + }, + { + "pk": 29, + "model": "auth.user", + "fields": { + "username": "editor", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "last_login": "2011-04-24 11:31:50", + "groups": [ + 1 + ], + "user_permissions": [], + "password": "sha1$3be01$b6aa05c61fc52edae3055c55e160d4cfd4756d91", + "email": "editor@exapmle.com", + "date_joined": "2011-04-24 11:31:50" + } + }, + { + "pk": 27, + "model": "auth.user", + "fields": { + "username": "nonactive", + "first_name": "", + "last_name": "", + "is_active": false, + "is_superuser": false, + "is_staff": false, + "last_login": "2011-04-24 11:31:00", + "groups": [], + "user_permissions": [], + "password": "sha1$0bf10$d60f146d15e4fe3cb0de5a607a17902d0f63a95c", + "email": "", + "date_joined": "2011-04-24 11:31:00" + } + }, + { + "pk": 28, + "model": "auth.user", + "fields": { + "username": "staff", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "last_login": "2011-04-24 11:31:26", + "groups": [], + "user_permissions": [], + "password": "sha1$5df85$bb4c1894a866fb86465d28831000af20316233d5", + "email": "staff@example.com", + "date_joined": "2011-04-24 11:31:26" + } + } +] \ No newline at end of file diff --git a/cas_provider/forms.py b/cas_provider/forms.py index d9974a2..8ec7a22 100644 --- a/cas_provider/forms.py +++ b/cas_provider/forms.py @@ -1,7 +1,14 @@ from django import forms +from django.conf import settings +from django.contrib.auth import authenticate +from django.contrib.auth.forms import AuthenticationForm +from django.forms import ValidationError +from django.utils.translation import ugettext_lazy as _ +from models import LoginTicket +import datetime -class LoginForm(forms.Form): +class LoginForm(AuthenticationForm): email = forms.CharField(widget=forms.TextInput(attrs={'autofocus': 'autofocus', 'max_length': '255'})) password = forms.CharField(widget=forms.PasswordInput) diff --git a/cas_provider/locale/ru/LC_MESSAGES/django.mo b/cas_provider/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000..c02ed63 Binary files /dev/null and b/cas_provider/locale/ru/LC_MESSAGES/django.mo differ diff --git a/cas_provider/locale/ru/LC_MESSAGES/django.po b/cas_provider/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000..42a8506 --- /dev/null +++ b/cas_provider/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Alex Kamedov , 2011. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-04-24 18:51+0600\n" +"PO-Revision-Date: 2011-04-07 12:01+0600\n" +"Last-Translator: Volf \n" +"Language-Team: delux\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Virtaal 0.6.1\n" + +#: forms.py:14 +msgid "username" +msgstr "имя пользователя" + +#: forms.py:15 +msgid "password" +msgstr "пароль" + +#: forms.py:30 +msgid "Login ticket expired. Please try again." +msgstr "Истек срок действия билета входа. Пожалуйста, попробуйте еще раз." + +#: forms.py:38 +msgid "Incorrect username and/or password." +msgstr "Неверное имя пользователя и/или пароль." + +#: forms.py:40 +msgid "This account is disabled." +msgstr "Эта учетная запись отключена." + +#: models.py:14 +msgid "ticket" +msgstr "билет" + +#: models.py:15 +msgid "created" +msgstr "создан" + +#: models.py:34 +msgid "user" +msgstr "пользователь" + +#: models.py:35 +msgid "service" +msgstr "сервис" + +#: models.py:40 +msgid "Service Ticket" +msgstr "Билет для сервиса" + +#: models.py:41 +msgid "Service Tickets" +msgstr "Билеты для сервисов" + +#: models.py:59 +msgid "Login Ticket" +msgstr "Билет для входа" + +#: models.py:60 +msgid "Login Tickets" +msgstr "Билеты для входа" diff --git a/cas_provider/management/commands/cleanuptickets.py b/cas_provider/management/commands/cleanuptickets.py index 772fdcb..401bec1 100644 --- a/cas_provider/management/commands/cleanuptickets.py +++ b/cas_provider/management/commands/cleanuptickets.py @@ -8,7 +8,6 @@ contains the actual logic for determining which accounts are deleted. """ from django.core.management.base import NoArgsCommand -from django.core.management.base import CommandError from django.conf import settings import datetime diff --git a/cas_provider/migrations/0001_initial.py b/cas_provider/migrations/0001_initial.py new file mode 100644 index 0000000..74770af --- /dev/null +++ b/cas_provider/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# encoding: utf-8 +import 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 'ServiceTicket' + db.create_table('cas_provider_serviceticket', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('ticket', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('service', self.gf('django.db.models.fields.URLField')(max_length=200)), + )) + db.send_create_signal('cas_provider', ['ServiceTicket']) + + # Adding model 'LoginTicket' + db.create_table('cas_provider_loginticket', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('ticket', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal('cas_provider', ['LoginTicket']) + + + def backwards(self, orm): + + # Deleting model 'ServiceTicket' + db.delete_table('cas_provider_serviceticket') + + # Deleting model 'LoginTicket' + db.delete_table('cas_provider_loginticket') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'cas_provider.loginticket': { + 'Meta': {'object_name': 'LoginTicket'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'cas_provider.serviceticket': { + 'Meta': {'object_name': 'ServiceTicket'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['cas_provider'] diff --git a/cas_provider/migrations/0002_auto__add_proxygrantingticket__add_proxyticket__add_proxygrantingticke.py b/cas_provider/migrations/0002_auto__add_proxygrantingticket__add_proxyticket__add_proxygrantingticke.py new file mode 100644 index 0000000..46247aa --- /dev/null +++ b/cas_provider/migrations/0002_auto__add_proxygrantingticket__add_proxyticket__add_proxygrantingticke.py @@ -0,0 +1,125 @@ +# encoding: utf-8 +import 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 'ProxyGrantingTicket' + db.create_table('cas_provider_proxygrantingticket', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('ticket', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('serviceTicket', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['cas_provider.ServiceTicket'], null=True)), + ('pgtiou', self.gf('django.db.models.fields.CharField')(max_length=256)), + ('targetService', self.gf('django.db.models.fields.URLField')(max_length=200)), + )) + db.send_create_signal('cas_provider', ['ProxyGrantingTicket']) + + # Adding model 'ProxyTicket' + db.create_table('cas_provider_proxyticket', ( + ('serviceticket_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cas_provider.ServiceTicket'], unique=True, primary_key=True)), + ('proxyGrantingTicket', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['cas_provider.ProxyGrantingTicket'])), + )) + db.send_create_signal('cas_provider', ['ProxyTicket']) + + # Adding model 'ProxyGrantingTicketIOU' + db.create_table('cas_provider_proxygrantingticketiou', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('ticket', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('proxyGrantingTicket', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['cas_provider.ProxyGrantingTicket'])), + )) + db.send_create_signal('cas_provider', ['ProxyGrantingTicketIOU']) + + + def backwards(self, orm): + + # Deleting model 'ProxyGrantingTicket' + db.delete_table('cas_provider_proxygrantingticket') + + # Deleting model 'ProxyTicket' + db.delete_table('cas_provider_proxyticket') + + # Deleting model 'ProxyGrantingTicketIOU' + db.delete_table('cas_provider_proxygrantingticketiou') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'cas_provider.loginticket': { + 'Meta': {'object_name': 'LoginTicket'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'cas_provider.proxygrantingticket': { + 'Meta': {'object_name': 'ProxyGrantingTicket'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pgtiou': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'serviceTicket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cas_provider.ServiceTicket']", 'null': 'True'}), + 'targetService': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'cas_provider.proxygrantingticketiou': { + 'Meta': {'object_name': 'ProxyGrantingTicketIOU'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'proxyGrantingTicket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cas_provider.ProxyGrantingTicket']"}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'cas_provider.proxyticket': { + 'Meta': {'object_name': 'ProxyTicket', '_ormbases': ['cas_provider.ServiceTicket']}, + 'proxyGrantingTicket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cas_provider.ProxyGrantingTicket']"}), + 'serviceticket_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cas_provider.ServiceTicket']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'cas_provider.serviceticket': { + 'Meta': {'object_name': 'ServiceTicket'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['cas_provider'] diff --git a/cas_provider/migrations/0003_auto__del_field_proxygrantingticket_targetService.py b/cas_provider/migrations/0003_auto__del_field_proxygrantingticket_targetService.py new file mode 100644 index 0000000..46b5124 --- /dev/null +++ b/cas_provider/migrations/0003_auto__del_field_proxygrantingticket_targetService.py @@ -0,0 +1,94 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting field 'ProxyGrantingTicket.targetService' + db.delete_column('cas_provider_proxygrantingticket', 'targetService') + + + def backwards(self, orm): + + # Adding field 'ProxyGrantingTicket.targetService' + db.add_column('cas_provider_proxygrantingticket', 'targetService', self.gf('django.db.models.fields.URLField')(default='http://not.used', max_length=200), keep_default=False) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'cas_provider.loginticket': { + 'Meta': {'object_name': 'LoginTicket'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'cas_provider.proxygrantingticket': { + 'Meta': {'object_name': 'ProxyGrantingTicket'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pgtiou': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'serviceTicket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cas_provider.ServiceTicket']", 'null': 'True'}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'cas_provider.proxygrantingticketiou': { + 'Meta': {'object_name': 'ProxyGrantingTicketIOU'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'proxyGrantingTicket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cas_provider.ProxyGrantingTicket']"}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}) + }, + 'cas_provider.proxyticket': { + 'Meta': {'object_name': 'ProxyTicket', '_ormbases': ['cas_provider.ServiceTicket']}, + 'proxyGrantingTicket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cas_provider.ProxyGrantingTicket']"}), + 'serviceticket_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cas_provider.ServiceTicket']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'cas_provider.serviceticket': { + 'Meta': {'object_name': 'ServiceTicket'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'ticket': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['cas_provider'] diff --git a/cas_provider/migrations/__init__.py b/cas_provider/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cas_provider/models.py b/cas_provider/models.py index 39641a1..8306d3c 100644 --- a/cas_provider/models.py +++ b/cas_provider/models.py @@ -1,20 +1,99 @@ -from django.db import models from django.contrib.auth.models import User +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from random import Random +import string +import urllib +import urlparse +if hasattr(urlparse, 'parse_qs'): + parse_qs = urlparse.parse_qs +else: + # Python <2.6 compatibility + from cgi import parse_qs -class ServiceTicket(models.Model): - user = models.ForeignKey(User) - service = models.URLField(verify_exists=False) - ticket = models.CharField(max_length=256) - created = models.DateTimeField(auto_now=True) - - def __unicode__(self): - return "%s (%s) - %s" % (self.user.username, self.service, self.created) +__all__ = ['ServiceTicket', 'LoginTicket', 'ProxyGrantingTicket', 'ProxyTicket', 'ProxyGrantingTicketIOU'] + +class BaseTicket(models.Model): + ticket = models.CharField(_('ticket'), max_length=32) + created = models.DateTimeField(_('created'), auto_now=True) + class Meta: + abstract = True + + def __init__(self, *args, **kwargs): + if 'ticket' not in kwargs: + kwargs['ticket'] = self._generate_ticket() + super(BaseTicket, self).__init__(*args, **kwargs) -class LoginTicket(models.Model): - ticket = models.CharField(max_length=32) - created = models.DateTimeField(auto_now=True) - def __unicode__(self): - return "%s - %s" % (self.ticket, self.created) + return self.ticket + + def _generate_ticket(self, length=ticket.max_length, chars=string.ascii_letters + string.digits): + """ Generates a random string of the requested length. Used for creation of tickets. """ + return u"%s-%s" % (self.prefix, ''.join(Random().sample(chars, length - (len(self.prefix) + 1)))) + + +class ServiceTicket(BaseTicket): + user = models.ForeignKey(User, verbose_name=_('user')) + service = models.URLField(_('service'), verify_exists=False) + + prefix = 'ST' + + class Meta: + verbose_name = _('Service Ticket') + verbose_name_plural = _('Service Tickets') + + def get_redirect_url(self): + parsed = urlparse.urlparse(self.service) + query = parse_qs(parsed.query) + query['ticket'] = [self.ticket] + query = [((k, v) if len(v) > 1 else (k, v[0])) for k, v in query.iteritems()] + parsed = urlparse.ParseResult(parsed.scheme, parsed.netloc, + parsed.path, parsed.params, + urllib.urlencode(query), parsed.fragment) + return parsed.geturl() + + +class LoginTicket(BaseTicket): + prefix = 'LT' + + class Meta: + verbose_name = _('Login Ticket') + verbose_name_plural = _('Login Tickets') + + +class ProxyGrantingTicket(BaseTicket): + serviceTicket = models.ForeignKey(ServiceTicket, null=True) + pgtiou = models.CharField(max_length=256, verbose_name=_('PGTiou')) + prefix = 'PGT' + + def __init__(self, *args, **kwargs): + if 'pgtiou' not in kwargs: + kwargs['pgtiou'] = u"PGTIOU-%s" % (''.join(Random().sample(string.ascii_letters + string.digits, 50))) + super(ProxyGrantingTicket, self).__init__(*args, **kwargs) + + class Meta: + verbose_name = _('Proxy Granting Ticket') + verbose_name_plural = _('Proxy Granting Tickets') + + +class ProxyTicket(ServiceTicket): + proxyGrantingTicket = models.ForeignKey(ProxyGrantingTicket, verbose_name=_('Proxy Granting Ticket')) + + prefix = 'PT' + + class Meta: + verbose_name = _('Proxy Ticket') + verbose_name_plural = _('Proxy Tickets') + + +class ProxyGrantingTicketIOU(BaseTicket): + proxyGrantingTicket = models.ForeignKey(ProxyGrantingTicket, verbose_name=_('Proxy Granting Ticket')) + + prefix = 'PGTIOU' + + class Meta: + verbose_name = _('Proxy Granting Ticket IOU') + verbose_name_plural = _('Proxy Granting Tickets IOU') + 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 new file mode 100644 index 0000000..ca9e561 --- /dev/null +++ b/cas_provider/templates/cas/warn.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block title %} +Warning +{% endblock %} + +{% block content %} + +
    + Confirm to log in to {{ service }} + +

    +
    + +{% endblock %} diff --git a/cas_provider/tests.py b/cas_provider/tests.py new file mode 100644 index 0000000..46ba3ca --- /dev/null +++ b/cas_provider/tests.py @@ -0,0 +1,351 @@ +import StringIO +import urllib2 +from xml import etree +from xml.etree import ElementTree +import cas_provider +from cas_provider.attribute_formatters import CAS, NSMAP +from cas_provider.models import ServiceTicket +from cas_provider.views import _cas2_sucess_response, INVALID_TICKET, _cas2_error_response, generate_proxy_granting_ticket +from django.contrib.auth.models import User, UserManager +from django.core.urlresolvers import reverse +from django.test import TestCase +from urlparse import urlparse, parse_qsl, parse_qs +from django.conf import settings + + + + +dummy_urlopen_url = None + + +def dummy_urlopen(url): + cas_provider.tests.dummy_urlopen_url = url + pass + +class ViewsTest(TestCase): + + fixtures = ['cas_users', ] + + + + def setUp(self): + self.service = 'http://example.com/' + + + def test_successful_login_with_proxy(self): + urllib2.urlopen = dummy_urlopen # monkey patching urllib2.urlopen so that the testcase doesnt really opens a url + proxyTarget = "http://my.sweet.service" + + response = self._login_user('root', '123') + response = self._validate_cas2(response, True, proxyTarget ) + + # Test: I'm acting as the service that will call another service + # Step 1: Get the proxy granting ticket + responseXml = ElementTree.parse(StringIO.StringIO(response.content)) + auth_success = responseXml.find(CAS + 'authenticationSuccess', namespaces=NSMAP) + pgt = auth_success.find(CAS + "proxyGrantingTicket", namespaces=NSMAP) + user = auth_success.find(CAS + "user", namespaces=NSMAP) + self.assertEqual('root', user.text) + self.assertIsNotNone(pgt.text) + self.assertTrue(pgt.text.startswith('PGTIOU')) + + pgtId = parse_qs(urlparse(cas_provider.tests.dummy_urlopen_url).query)['pgtId'] + + #Step 2: Get the actual proxy ticket + proxyTicketResponse = self.client.get(reverse('proxy'), {'targetService': proxyTarget, 'pgt': pgtId}, follow=False) + proxyTicketResponseXml = ElementTree.parse(StringIO.StringIO(proxyTicketResponse.content)) + self.assertIsNotNone(proxyTicketResponseXml.find(CAS + "proxySuccess", namespaces=NSMAP)) + self.assertIsNotNone(proxyTicketResponseXml.find(CAS + "proxySuccess/cas:proxyTicket", namespaces=NSMAP)) + proxyTicket = proxyTicketResponseXml.find(CAS + "proxySuccess/cas:proxyTicket", namespaces=NSMAP); + + #Step 3: I have the proxy ticket I can talk to some other backend service as the currently logged in user! + proxyValidateResponse = self.client.get(reverse('cas_proxy_validate'), {'ticket': proxyTicket.text, 'service': proxyTarget}) + proxyValidateResponseXml = ElementTree.parse(StringIO.StringIO(proxyValidateResponse.content)) + + auth_success_2 = proxyValidateResponseXml.find(CAS + 'authenticationSuccess', namespaces=NSMAP) + user_2 = auth_success.find(CAS + "user", namespaces=NSMAP) + proxies_1 = auth_success_2.find(CAS + "proxies") + self.assertIsNone(proxies_1) # there are no proxies. I am issued by a Service Ticket + + self.assertEqual(user.text, user_2.text) + + + def test_successful_proxy_chaining(self): + urllib2.urlopen = dummy_urlopen # monkey patching urllib2.urlopen so that the testcase doesnt really opens a url + proxyTarget_1 = "http://my.sweet.service_1" + proxyTarget_2 = "http://my.sweet.service_2" + + response = self._login_user('root', '123') + response = self._validate_cas2(response, True, proxyTarget_1 ) + + # Test: I'm acting as the service that will call another service + # Step 1: Get the proxy granting ticket + responseXml = ElementTree.parse(StringIO.StringIO(response.content)) + auth_success_1 = responseXml.find(CAS + 'authenticationSuccess', namespaces=NSMAP) + pgt_1 = auth_success_1.find(CAS + "proxyGrantingTicket", namespaces=NSMAP) + user_1 = auth_success_1.find(CAS + "user", namespaces=NSMAP) + self.assertEqual('root', user_1.text) + self.assertIsNotNone(pgt_1.text) + self.assertTrue(pgt_1.text.startswith('PGTIOU')) + + pgtId_1 = parse_qs(urlparse(cas_provider.tests.dummy_urlopen_url).query)['pgtId'] + + #Step 2: Get the actual proxy ticket + proxyTicketResponse_1 = self.client.get(reverse('proxy'), {'targetService': proxyTarget_1, 'pgt': pgtId_1}, follow=False) + proxyTicketResponseXml_1 = ElementTree.parse(StringIO.StringIO(proxyTicketResponse_1.content)) + self.assertIsNotNone(proxyTicketResponseXml_1.find(CAS + "proxySuccess", namespaces=NSMAP)) + self.assertIsNotNone(proxyTicketResponseXml_1.find(CAS + "proxySuccess/cas:proxyTicket", namespaces=NSMAP)) + proxyTicket_1 = proxyTicketResponseXml_1.find(CAS + "proxySuccess/cas:proxyTicket", namespaces=NSMAP); + + #Step 3: I'm backend service 1 - I have the proxy ticket - I want to talk to back service 2 + # + proxyValidateResponse_1 = self.client.get(reverse('cas_proxy_validate'), {'ticket': proxyTicket_1.text, 'service': proxyTarget_1, 'pgtUrl': proxyTarget_2}) + proxyValidateResponseXml_1 = ElementTree.parse(StringIO.StringIO(proxyValidateResponse_1.content)) + + auth_success_2 = proxyValidateResponseXml_1.find(CAS + 'authenticationSuccess', namespaces=NSMAP) + user_2 = auth_success_2.find(CAS + "user", namespaces=NSMAP) + + proxies_1 = auth_success_2.find(CAS + "proxies") + self.assertIsNone(proxies_1) # there are no proxies. I am issued by a Service Ticket + self.assertIsNotNone(auth_success_2) + self.assertEqual('root', user_2.text) + + pgt_2 = auth_success_2.find(CAS + "proxyGrantingTicket", namespaces=NSMAP) + user = auth_success_2.find(CAS + "user", namespaces=NSMAP) + self.assertEqual('root', user.text) + self.assertIsNotNone(pgt_2.text) + self.assertTrue(pgt_2.text.startswith('PGTIOU')) + + pgtId_2 = parse_qs(urlparse(cas_provider.tests.dummy_urlopen_url).query)['pgtId'] + + #Step 4: Get the second proxy ticket + proxyTicketResponse_2 = self.client.get(reverse('proxy'), {'targetService': proxyTarget_2, 'pgt': pgtId_2}) + proxyTicketResponseXml_2 = ElementTree.parse(StringIO.StringIO(proxyTicketResponse_2.content)) + self.assertIsNotNone(proxyTicketResponseXml_2.find(CAS + "proxySuccess", namespaces=NSMAP)) + self.assertIsNotNone(proxyTicketResponseXml_2.find(CAS + "proxySuccess/cas:proxyTicket", namespaces=NSMAP)) + proxyTicket_2 = proxyTicketResponseXml_2.find(CAS + "proxySuccess/cas:proxyTicket", namespaces=NSMAP) + + proxyValidateResponse_3 = self.client.get(reverse('cas_proxy_validate'), {'ticket': proxyTicket_2.text, 'service': proxyTarget_2, 'pgtUrl': None}) + proxyValidateResponseXml_3 = ElementTree.parse(StringIO.StringIO(proxyValidateResponse_3.content)) + + auth_success_3 = proxyValidateResponseXml_3.find(CAS + 'authenticationSuccess', namespaces=NSMAP) + user_3 = auth_success_3.find(CAS + "user", namespaces=NSMAP) + + proxies_3 = auth_success_3.find(CAS + "proxies") + self.assertIsNotNone(proxies_3) # there should be a proxy. I am issued by a Proxy Ticket + proxy_3 = proxies_3.find(CAS + "proxy", namespaces=NSMAP) + self.assertEqual(proxyTarget_1, proxy_3.text ) + + self.assertIsNotNone(auth_success_2) + self.assertEqual('root', user_3.text) + + + + + def test_succeessful_login(self): + response = self._login_user('root', '123') + self._validate_cas1(response, True) + + response = self.client.get(reverse('cas_login'), {'service': self.service}, follow=False) + self.assertEqual(response.status_code, 302) + self.assertTrue(response['location'].startswith('%s?ticket=' % self.service)) + + response = self.client.get(reverse('cas_login'), follow=False) + self.assertEqual(response.status_code, 302) + self.assertTrue(response['location'].startswith('http://testserver/')) + + response = self.client.get(response['location'], follow=False) + self.assertIn(response.status_code, [302, 200]) + + response = self.client.get(reverse('cas_login'), {'service': self.service, 'warn': True}, follow=False) + self.assertEqual(response.status_code, 200) + 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) + + self._cas_logout() + + response = self.client.get(reverse('cas_login'), follow=False) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].is_anonymous(), True) + + + def test_broken_pwd(self): + self._fail_login('root', '321') + + def test_broken_username(self): + self._fail_login('notroot', '123') + + def test_nonactive_user_login(self): + self._fail_login('nonactive', '123') + + def test_cas2_success_validate(self): + response = self._login_user('root', '123') + 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')): + response = self._login_user(user, pwd) + self._validate_cas2(response, False) + + + def test_generate_proxy_granting_ticket(self): + urllib2.urlopen = dummy_urlopen # monkey patching urllib2.urlopen so that the testcase doesnt really opens a url + url = 'http://my.call.back/callhere' + + user = User.objects.get(username = 'root') + st = ServiceTicket.objects.create(user = user ) + pgt = generate_proxy_granting_ticket(url, st) + self.assertIsNotNone(pgt) + + calledUrl = cas_provider.tests.dummy_urlopen_url + parsedUrl = urlparse(calledUrl) + params = parse_qs(parsedUrl.query) + self.assertIsNotNone(params['pgtId']) + self.assertIsNotNone(params['pgtIou']) + + + + def _fail_login(self, username, password): + response = self._login_user(username, password) + self._validate_cas1(response, False) + + response = self.client.get(reverse('cas_login'), {'service': self.service}, follow=False) + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse('cas_login'), follow=False) + self.assertEqual(response.status_code, 200) + + + + def _login_user(self, username, password): + self.username = username + response = self.client.get(reverse('cas_login'), {'service': self.service}) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'cas/login.html') + form = response.context['form'] + service = form['service'].value() + return self.client.post(reverse('cas_login'), { + 'username': username, + 'password': password, + 'lt': form['lt'].value(), + 'service': service + }, follow=False) + + + def _validate_cas1(self, response, is_correct=True): + if is_correct: + self.assertEqual(response.status_code, 302) + self.assertTrue(response.has_header('location')) + location = urlparse(response['location']) + ticket = location.query.split('=')[1] + + response = self.client.get(reverse('cas_validate'), {'ticket': ticket, 'service': self.service}, follow=False) + self.assertEqual(response.status_code, 200) + self.assertEqual(unicode(response.content), u'yes\n%s\n' % self.username) + else: + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['form'].errors), 1) + + response = self.client.get(reverse('cas_validate'), {'ticket': 'ST-12312312312312312312312', 'service': self.service}, follow=False) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, u'no\n\n') + + + def _validate_cas2(self, response, is_correct=True, pgtUrl = None): + if is_correct: + self.assertEqual(response.status_code, 302) + self.assertTrue(response.has_header('location')) + location = urlparse(response['location']) + ticket = location.query.split('=')[1] + if pgtUrl: + response = self.client.get(reverse('cas_service_validate'), {'ticket': ticket, 'service': self.service, 'pgtUrl': pgtUrl}, follow=False) + else: + response = self.client.get(reverse('cas_service_validate'), {'ticket': ticket, 'service': self.service}, follow=False) + self.assertEqual(response.status_code, 200) + else: + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['form'].errors), 1) + + 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): + + fixtures = ['cas_users.json', ] + + def setUp(self): + self.user = User.objects.get(username='root') + + def test_redirects(self): + 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/urls.py b/cas_provider/urls.py index f91786b..154aac0 100644 --- a/cas_provider/urls.py +++ b/cas_provider/urls.py @@ -1,11 +1,13 @@ -from django.conf.urls.defaults import * +from django.conf.urls.defaults import patterns, url -from views import * -urlpatterns = patterns('', - url(r'^login/', login), - url(r'^socialauth-login/$', socialauth_login), - url(r'^validate/', validate), - url(r'^logout/', logout), - url(r'^login/merge/', login, {'merge': True, 'template_name': 'cas/merge.html'}) - ) +urlpatterns = patterns('cas_provider.views', + url(r'^login/merge/', 'login', {'merge': True, 'template_name': 'cas/merge.html'}) + url(r'^login/?$', 'login', name='cas_login'), + url(r'^socialauth-login/$', 'socialauth_login', name='cas_socialauth_login'), + url(r'^validate/?$', 'validate', name='cas_validate'), + url(r'^proxy/?$', 'proxy', name='proxy'), + url(r'^serviceValidate/?$', 'service_validate', name='cas_service_validate'), + url(r'^proxyValidate/?$', 'proxy_validate', name='cas_proxy_validate'), + url(r'^logout/?$', 'logout', name='cas_logout'), +) diff --git a/cas_provider/utils.py b/cas_provider/utils.py deleted file mode 100644 index 04a5c12..0000000 --- a/cas_provider/utils.py +++ /dev/null @@ -1,24 +0,0 @@ -from random import Random -import string - -from models import ServiceTicket, LoginTicket - -def _generate_string(length=8, chars=string.ascii_letters + string.digits): - """ Generates a random string of the requested length. Used for creation of tickets. """ - return ''.join(Random().sample(chars, length)) - -def create_service_ticket(user, service): - """ Creates a new service ticket for the specified user and service. - Uses _generate_string. - """ - ticket_string = 'ST-' + _generate_string(29) # Total ticket length = 29 + 3 = 32 - ticket = ServiceTicket(service=service, user=user, ticket=ticket_string) - ticket.save() - return ticket - -def create_login_ticket(): - """ Creates a new login ticket for the login form. Uses _generate_string. """ - ticket_string = 'LT-' + _generate_string(29) - ticket = LoginTicket(ticket=ticket_string) - ticket.save() - return ticket_string \ No newline at end of file diff --git a/cas_provider/views.py b/cas_provider/views.py index 5ba62e6..a0847b9 100644 --- a/cas_provider/views.py +++ b/cas_provider/views.py @@ -2,31 +2,51 @@ import logging logger = logging.getLogger('cas_provider.views') import urllib +import logging +from urllib import urlencode +import urllib2 +import urlparse + from django.http import HttpResponse, HttpResponseRedirect +from django.conf import settings +from django.contrib.auth import login as auth_login, logout as auth_logout +from django.core.urlresolvers import get_callable from django.shortcuts import render_to_response from django.template import RequestContext from django.contrib.auth import authenticate -from django.contrib.auth import login as auth_login, logout as auth_logout from django.core.urlresolvers import reverse -from forms import LoginForm, MergeLoginForm -from models import ServiceTicket -from utils import create_service_ticket -from exceptions import SameEmailMismatchedPasswords +from lxml import etree +from cas_provider.attribute_formatters import NSMAP, CAS +from cas_provider.models import ProxyGrantingTicket, ProxyTicket +from cas_provider.models import ServiceTicket + +from cas_provider.exceptions import SameEmailMismatchedPasswords +from cas_provider.forms import LoginForm, MergeLoginForm from . import signals -__all__ = ['login', 'validate', 'logout'] +__all__ = ['login', 'validate', 'logout', 'service_validate'] +INVALID_TICKET = 'INVALID_TICKET' +INVALID_SERVICE = 'INVALID_SERVICE' +INVALID_REQUEST = 'INVALID_REQUEST' +INTERNAL_ERROR = 'INTERNAL_ERROR' + +ERROR_MESSAGES = ( + (INVALID_TICKET, u'The provided ticket is invalid.'), + (INVALID_SERVICE, u'Service is invalid'), + (INVALID_REQUEST, u'Not all required parameters were sent.'), + (INTERNAL_ERROR, u'An internal error occurred during ticket validation'), + ) -def _build_service_url(service, ticket): - if service.find('?') == -1: - return service + '?ticket=' + ticket - else: - return service + '&ticket=' + ticket +logger = logging.getLogger(__name__) -def login(request, template_name='cas/login.html', success_redirect='/account/', **kwargs): + +def login(request, template_name='cas/login.html', + success_redirect=settings.LOGIN_REDIRECT_URL, + warn_template_name='cas/warn.html', **kwargs): merge = kwargs.get('merge', False) logging.debug('CAS Provider Login view. Method is %s, merge is %s, template is %s.', request.method, merge, template_name) @@ -108,13 +128,19 @@ def login(request, template_name='cas/login.html', success_redirect='/account/', logging.debug('Redirecting to %s', success_redirect) return HttpResponseRedirect(success_redirect) else: + if request.GET.get('warn', False): + return render_to_response(warn_template_name, { + 'service': service, + 'warn': False + }, context_instance=RequestContext(request)) + # Create a service ticket and redirect to the service. - ticket = create_service_ticket(request.user, service) + ticket = ServiceTicket.objects.create(service=service, user=user) if 'service' in request.session: # Don't need this any more. del request.session['service'] - url = _build_service_url(service, ticket.ticket) + url = ticket.get_redirect_url() logging.debug('Redirecting to %s', url) return HttpResponseRedirect(url) @@ -123,12 +149,18 @@ def login(request, template_name='cas/login.html', success_redirect='/account/', def validate(request): + """Validate ticket via CAS v.1 protocol + """ service = request.GET.get('service', None) ticket_string = request.GET.get('ticket', None) logger.info('Validating ticket %s for %s', ticket_string, service) if service is not None and ticket_string is not None: + #renew = request.GET.get('renew', True) + #if not renew: + # TODO: check user SSO session try: ticket = ServiceTicket.objects.get(ticket=ticket_string) + assert ticket.service == service except ServiceTicket.DoesNotExist: logger.exception("Tried to validate with an invalid ticket %s for %s", ticket_string, service) except Exception as e: @@ -146,7 +178,178 @@ def validate(request): return HttpResponse("no\n\n") -def logout(request, template_name='cas/logout.html'): +def logout(request, template_name='cas/logout.html', + auto_redirect=settings.CAS_AUTO_REDIRECT_AFTER_LOGOUT): url = request.GET.get('url', None) - auth_logout(request) - return render_to_response(template_name, {'url': url}, context_instance=RequestContext(request)) + if request.user.is_authenticated(): + for ticket in ServiceTicket.objects.filter(user=request.user): + ticket.delete() + auth_logout(request) + if url and auto_redirect: + return HttpResponseRedirect(url) + return render_to_response(template_name, {'url': url}, + context_instance=RequestContext(request)) + + +def proxy(request): + targetService = request.GET['targetService'] + pgt_id = request.GET['pgt'] + + try: + proxyGrantingTicket = ProxyGrantingTicket.objects.get(ticket=pgt_id) + except ProxyGrantingTicket.DoesNotExist: + return _cas2_error_response(INVALID_TICKET) + + pt = ProxyTicket.objects.create(proxyGrantingTicket=proxyGrantingTicket, + user=proxyGrantingTicket.serviceTicket.user, + service=targetService) + return _cas2_proxy_success(pt.ticket) + + +def ticket_validate(service, ticket_string, pgtUrl): + if service is None or ticket_string is None: + return _cas2_error_response(INVALID_REQUEST) + + try: + if ticket_string.startswith('ST'): + ticket = ServiceTicket.objects.get(ticket=ticket_string) + elif ticket_string.startswith('PT'): + ticket = ProxyTicket.objects.get(ticket=ticket_string) + else: + return _cas2_error_response(INVALID_TICKET, + '%(ticket)s is neither Service (ST-...) nor Proxy Ticket (PT-...)' % { + 'ticket': ticket_string}) + except ServiceTicket.DoesNotExist: + return _cas2_error_response(INVALID_TICKET) + + ticketUrl = urlparse.urlparse(ticket.service) + serviceUrl = urlparse.urlparse(service) + + if not(ticketUrl.hostname == serviceUrl.hostname and ticketUrl.path == serviceUrl.path and ticketUrl.port == serviceUrl.port): + return _cas2_error_response(INVALID_SERVICE) + + pgtIouId = None + proxies = () + if pgtUrl is not None: + pgt = generate_proxy_granting_ticket(pgtUrl, ticket) + if pgt: + pgtIouId = pgt.pgtiou + + if hasattr(ticket, 'proxyticket'): + pgt = ticket.proxyticket.proxyGrantingTicket + # I am issued by this proxy granting ticket + if hasattr(pgt.serviceTicket, 'proxyticket'): + while pgt: + if hasattr(pgt.serviceTicket, 'proxyticket'): + proxies += (pgt.serviceTicket.service,) + pgt = pgt.serviceTicket.proxyticket.proxyGrantingTicket + else: + pgt = None + + user = ticket.user + return _cas2_sucess_response(user, pgtIouId, proxies) + + +def service_validate(request): + """Validate ticket via CAS v.2 protocol""" + service = request.GET.get('service', None) + ticket_string = request.GET.get('ticket', None) + pgtUrl = request.GET.get('pgtUrl', None) + if ticket_string.startswith('PT-'): + return _cas2_error_response(INVALID_TICKET, "serviceValidate cannot verify proxy tickets") + else: + return ticket_validate(service, ticket_string, pgtUrl) + + +def proxy_validate(request): + """Validate ticket via CAS v.2 protocol""" + service = request.GET.get('service', None) + ticket_string = request.GET.get('ticket', None) + pgtUrl = request.GET.get('pgtUrl', None) + return ticket_validate(service, ticket_string, pgtUrl) + + +def generate_proxy_granting_ticket(pgt_url, ticket): + proxy_callback_good_status = (200, 202, 301, 302, 304) + uri = list(urlparse.urlsplit(pgt_url)) + + pgt = ProxyGrantingTicket() + pgt.serviceTicket = ticket + pgt.targetService = pgt_url + + if hasattr(ticket, 'proxyGrantingTicket'): + # here we got a proxy ticket! tata! + pgt.pgt = ticket.proxyGrantingTicket + + params = {'pgtId': pgt.ticket, 'pgtIou': pgt.pgtiou} + + query = dict(urlparse.parse_qsl(uri[4])) + query.update(params) + + uri[3] = urlencode(query) + + try: + response = urllib2.urlopen(urlparse.urlunsplit(uri)) + except urllib2.HTTPError as e: + if not e.code in proxy_callback_good_status: + logger.debug('Checking Proxy Callback URL {} returned {}. Not issuing PGT.'.format(uri, e.code)) + return + except urllib2.URLError as e: + logger.debug('Checking Proxy Callback URL {} raised URLError. Not issuing PGT.'.format(uri)) + return + + pgt.save() + return pgt + + +def _cas2_proxy_success(pt): + return HttpResponse(proxy_success(pt)) + + +def _cas2_sucess_response(user, pgt=None, proxies=None): + return HttpResponse(auth_success_response(user, pgt, proxies), mimetype='text/xml') + + +def _cas2_error_response(code, message=None): + return HttpResponse(u''' + + %(message)s + + ''' % { + 'code': code, + 'message': message if message else dict(ERROR_MESSAGES).get(code) + }, mimetype='text/xml') + + +def proxy_success(pt): + response = etree.Element(CAS + 'serviceResponse', nsmap=NSMAP) + proxySuccess = etree.SubElement(response, CAS + 'proxySuccess') + proxyTicket = etree.SubElement(proxySuccess, CAS + 'proxyTicket') + proxyTicket.text = pt + return unicode(etree.tostring(response, encoding='utf-8'), 'utf-8') + + +def auth_success_response(user, pgt, proxies): + 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) + + if pgt: + pgtElement = etree.SubElement(auth_success, CAS + 'proxyGrantingTicket') + pgtElement.text = pgt + + if proxies: + proxiesElement = etree.SubElement(auth_success, CAS + "proxies") + for proxy in proxies: + proxyElement = etree.SubElement(proxiesElement, CAS + "proxy") + proxyElement.text = proxy + + return unicode(etree.tostring(response, encoding='utf-8'), 'utf-8') diff --git a/cas_provider_examples/__init__.py b/cas_provider_examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cas_provider_examples/simple/__init__.py b/cas_provider_examples/simple/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cas_provider_examples/simple/manage.py b/cas_provider_examples/simple/manage.py new file mode 100644 index 0000000..3e4eedc --- /dev/null +++ b/cas_provider_examples/simple/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +import imp +try: + imp.find_module('settings') # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) + sys.exit(1) + +import settings + +if __name__ == "__main__": + execute_manager(settings) diff --git a/cas_provider_examples/simple/settings.py b/cas_provider_examples/simple/settings.py new file mode 100644 index 0000000..209927c --- /dev/null +++ b/cas_provider_examples/simple/settings.py @@ -0,0 +1,142 @@ +# Django settings for xxx project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +import os + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(os.path.realpath(__file__), 'db'), + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'kv*6pmkq47crqskw%wkst!h7xnisy78zzli@rtklgm#y6o=of!' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'simple.urls' + +import os +PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) + +TEMPLATE_DIRS = ( + os.path.join(PROJECT_PATH, 'templates') +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'cas_provider', + 'south', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} diff --git a/cas_provider_examples/simple/templates/base.html b/cas_provider_examples/simple/templates/base.html new file mode 100644 index 0000000..6bd902c --- /dev/null +++ b/cas_provider_examples/simple/templates/base.html @@ -0,0 +1,13 @@ + + + + {% block title %}{% endblock %} + + +
    + {% block content %}{% endblock %} +
    + + + \ No newline at end of file diff --git a/cas_provider_examples/simple/templates/login-success-redirect-target.html b/cas_provider_examples/simple/templates/login-success-redirect-target.html new file mode 100644 index 0000000..4ea8caa --- /dev/null +++ b/cas_provider_examples/simple/templates/login-success-redirect-target.html @@ -0,0 +1 @@ +yeah - success \ No newline at end of file diff --git a/cas_provider_examples/simple/urls.py b/cas_provider_examples/simple/urls.py new file mode 100644 index 0000000..c717237 --- /dev/null +++ b/cas_provider_examples/simple/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls.defaults import patterns, include, url + +import cas_provider +from django.views.generic.simple import redirect_to, direct_to_template + +urlpatterns = patterns('', + url(r'^', include('cas_provider.urls')), + url(r'^accounts/profile', direct_to_template, {'template': 'login-success-redirect-target.html'}), + + ) diff --git a/setup.py b/setup.py index 8f06deb..fe96615 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,26 @@ - +import os from setuptools import setup, find_packages - + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + setup( name='django-cas-provider', - version='0.1dev', + version='0.3.2', description='A "provider" for the Central Authentication Service (http://jasig.org/cas)', - author='Chris Williams', - author_email='chris@nitron.org', - url='http://nitron.org/', + author='(Chris Williams), Sebastian Annies', + author_email='(chris@nitron.org), sebastian.annies@googlemail.com', + url='https://github.com/castlabs/django-cas-provider', packages=find_packages(), + include_package_data=True, + license='MIT', + long_description=read('README.rst'), zip_safe=False, - install_requires=['setuptools'], + install_requires=['setuptools', + 'south>=0.7.2',], + classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Django", + "License :: OSI Approved :: MIT License", + ] )