From: Radek Czajka Date: Sun, 21 Sep 2014 20:40:55 +0000 (+0200) Subject: Merge branch 'elep' X-Git-Tag: 22.4~10 X-Git-Url: https://git.mdrn.pl/django-cas-provider.git/commitdiff_plain/38e127a205b49ab137d554895d426cb8e7dcd785?hp=a1a43977ac7fa00e65f80b8c3853e206b910599a Merge branch 'elep' --- diff --git a/.gitignore b/.gitignore index 6152084..e95e92d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ # Python garbage *.pyc *.egg-info +/build +/dist +.tox +.coverage +htmlcov # Mac OS X garbage .DS_Store diff --git a/MANIFEST.in b/MANIFEST.in index a87457c..d02b852 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ recursive-include cas_provider/templates *.html recursive-include cas_provider/fixtures * +recursive-include cas_provider/locale *.po *.mo include AUTHORS.txt include README.rst -include LICENSE \ No newline at end of file +include LICENSE diff --git a/README.rst b/README.rst index 258d685..d7bed27 100644 --- a/README.rst +++ b/README.rst @@ -19,29 +19,6 @@ 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 - - USAGE ====== diff --git a/cas_provider/__init__.py b/cas_provider/__init__.py index 1dd6c7a..ff16364 100644 --- a/cas_provider/__init__.py +++ b/cas_provider/__init__.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf import settings __all__ = [] @@ -8,7 +10,7 @@ _DEFAULTS = { 'CAS_AUTO_REDIRECT_AFTER_LOGOUT': False, } -for key, value in _DEFAULTS.iteritems(): +for key, value in _DEFAULTS.items(): try: getattr(settings, key) except AttributeError: diff --git a/cas_provider/admin.py b/cas_provider/admin.py index 499fa43..075fcb9 100644 --- a/cas_provider/admin.py +++ b/cas_provider/admin.py @@ -1,5 +1,7 @@ +from __future__ import unicode_literals + from django.contrib import admin -from models import * +from .models import * class ServiceTicketAdmin(admin.ModelAdmin): diff --git a/cas_provider/attribute_formatters.py b/cas_provider/attribute_formatters.py index 55eb4dd..8574962 100644 --- a/cas_provider/attribute_formatters.py +++ b/cas_provider/attribute_formatters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from lxml import etree import collections @@ -6,11 +8,17 @@ NSMAP = {'cas': CAS_URI} CAS = '{%s}' % CAS_URI +try: + basestring +except NameError: + basestring = (str, bytes) + + 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(): + style.text = 'Jasig' + for name, value in sorted(attrs.items()): if isinstance(value, collections.Iterable) and not isinstance(value, basestring): for e in value: element = etree.SubElement(attributes, CAS + name) @@ -22,8 +30,8 @@ def jasig(auth_success, attrs): def ruby_cas(auth_success, attrs): style = etree.SubElement(auth_success, CAS + 'attraStyle') - style.text = u'RubyCAS' - for name, value in attrs.items(): + style.text = 'RubyCAS' + for name, value in sorted(attrs.items()): if isinstance(value, collections.Iterable) and not isinstance(value, basestring): for e in value: element = etree.SubElement(auth_success, CAS + name) @@ -34,8 +42,8 @@ def ruby_cas(auth_success, attrs): 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(): + etree.SubElement(auth_success, CAS + 'attribute', name='attraStyle', value='Name-Value') + for name, value in sorted(attrs.items()): if isinstance(value, collections.Iterable) and not isinstance(value, basestring): for e in value: etree.SubElement(auth_success, CAS + 'attribute', name=name, value=e) diff --git a/cas_provider/exceptions.py b/cas_provider/exceptions.py index 202eda8..fdb6251 100644 --- a/cas_provider/exceptions.py +++ b/cas_provider/exceptions.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """cas_provider.exceptions -- exceptions defined for CAS login workflows """ +from __future__ import unicode_literals class SameEmailMismatchedPasswords(Exception): pass diff --git a/cas_provider/forms.py b/cas_provider/forms.py index 5ddb057..e4df105 100644 --- a/cas_provider/forms.py +++ b/cas_provider/forms.py @@ -1,20 +1,15 @@ +from __future__ import unicode_literals + from django import forms -from django.conf import settings -from django.contrib.auth import authenticate -from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ -from models import LoginTicket -import datetime class LoginForm(forms.Form): - email = forms.CharField(widget=forms.TextInput(attrs={'autofocus': 'autofocus', - 'placeholder': 'Email', - 'max_length': '255'})) - password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': 'Password'})) + username = forms.CharField(widget=forms.TextInput(attrs={'autofocus': 'autofocus', + 'max_length': '255'}), + label=_('Username')) + password = forms.CharField(widget=forms.PasswordInput(), label=_('Password')) service = forms.CharField(widget=forms.HiddenInput, required=False) - remember_me = forms.BooleanField(required=False, label="Keep me signed in", - widget=forms.CheckboxInput(attrs={'class': 'remember_me'})) def __init__(self, *args, **kwargs): # renew = kwargs.pop('renew', None) @@ -23,11 +18,7 @@ class LoginForm(forms.Form): super(LoginForm, self).__init__(*args, **kwargs) self.request = request - def clean_remember_me(self): - remember = self.cleaned_data['remember_me'] - if not remember and self.request is not None: - self.request.session.set_expiry(0) - class MergeLoginForm(LoginForm): - email = forms.CharField(max_length=255, widget=forms.HiddenInput) + username = forms.CharField(max_length=255, widget=forms.HiddenInput, + label=_('Username')) diff --git a/cas_provider/locale/el/LC_MESSAGES/django.mo b/cas_provider/locale/el/LC_MESSAGES/django.mo new file mode 100644 index 0000000..d63198b Binary files /dev/null and b/cas_provider/locale/el/LC_MESSAGES/django.mo differ diff --git a/cas_provider/locale/el/LC_MESSAGES/django.po b/cas_provider/locale/el/LC_MESSAGES/django.po new file mode 100644 index 0000000..91df766 --- /dev/null +++ b/cas_provider/locale/el/LC_MESSAGES/django.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-12-01 13:57+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models.py:18 +msgid "ticket" +msgstr "" + +#: models.py:19 +msgid "created" +msgstr "" + +#: models.py:38 +msgid "user" +msgstr "" + +#: models.py:39 +msgid "service" +msgstr "" + +#: models.py:44 +msgid "Service Ticket" +msgstr "" + +#: models.py:45 +msgid "Service Tickets" +msgstr "" + +#: models.py:62 +msgid "Login Ticket" +msgstr "" + +#: models.py:63 +msgid "Login Tickets" +msgstr "" + +#: models.py:68 +msgid "PGTiou" +msgstr "" + +#: models.py:77 models.py:82 models.py:92 +msgid "Proxy Granting Ticket" +msgstr "" + +#: models.py:78 +msgid "Proxy Granting Tickets" +msgstr "" + +#: models.py:87 +msgid "Proxy Ticket" +msgstr "" + +#: models.py:88 +msgid "Proxy Tickets" +msgstr "" + +#: models.py:97 +msgid "Proxy Granting Ticket IOU" +msgstr "" + +#: models.py:98 +msgid "Proxy Granting Tickets IOU" +msgstr "" + +#: views.py:128 +msgid "Incorrect username and/or password." +msgstr "Λάθος όνομα χρήστη και/ή κωδικός πρόσβασης." + +#: views.py:142 +msgid "" +"This account is disabled. Please contact us if you feel it should be enabled " +"again." +msgstr "Ο λογαριασμός σας έχει απενεργοποιηθεί. Παρακαλώ επικοινωνήστε μαζί μας, " +"εάν επιθυμείτε να τον ενεργοποιήσετε ξανά." diff --git a/cas_provider/locale/pl/LC_MESSAGES/django.mo b/cas_provider/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..7701b54 Binary files /dev/null and b/cas_provider/locale/pl/LC_MESSAGES/django.mo differ diff --git a/cas_provider/locale/pl/LC_MESSAGES/django.po b/cas_provider/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000..241d1c8 --- /dev/null +++ b/cas_provider/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,98 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-04-15 10:51+0200\n" +"PO-Revision-Date: 2013-04-15 10:52+0100\n" +"Last-Translator: Radek Czajka \n" +"Language-Team: LANGUAGE \n" +"Language: \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==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: forms.py:8 +#: forms.py:22 +msgid "Username" +msgstr "Użytkownik" + +#: forms.py:9 +msgid "Password" +msgstr "Hasło" + +#: models.py:18 +msgid "ticket" +msgstr "" + +#: models.py:19 +msgid "created" +msgstr "" + +#: models.py:38 +msgid "user" +msgstr "" + +#: models.py:39 +msgid "service" +msgstr "" + +#: models.py:44 +msgid "Service Ticket" +msgstr "" + +#: models.py:45 +msgid "Service Tickets" +msgstr "" + +#: models.py:62 +msgid "Login Ticket" +msgstr "" + +#: models.py:63 +msgid "Login Tickets" +msgstr "" + +#: models.py:68 +msgid "PGTiou" +msgstr "" + +#: models.py:77 +#: models.py:82 +#: models.py:92 +msgid "Proxy Granting Ticket" +msgstr "" + +#: models.py:78 +msgid "Proxy Granting Tickets" +msgstr "" + +#: models.py:87 +msgid "Proxy Ticket" +msgstr "" + +#: models.py:88 +msgid "Proxy Tickets" +msgstr "" + +#: models.py:97 +msgid "Proxy Granting Ticket IOU" +msgstr "" + +#: models.py:98 +msgid "Proxy Granting Tickets IOU" +msgstr "" + +#: views.py:128 +msgid "Incorrect username and/or password." +msgstr "Błędny użytkownik i/lub hasło." + +#: views.py:142 +msgid "This account is disabled. Please contact us if you feel it should be enabled again." +msgstr "To konto jest nieaktywne. Prosimy o kontakt, jeśli uważasz że powinno zostać aktywowane." + diff --git a/cas_provider/management/.svn/all-wcprops b/cas_provider/management/.svn/all-wcprops deleted file mode 100644 index 1ea1964..0000000 --- a/cas_provider/management/.svn/all-wcprops +++ /dev/null @@ -1,11 +0,0 @@ -K 25 -svn:wc:ra_dav:version-url -V 47 -/svn/!svn/ver/170/trunk/registration/management -END -__init__.py -K 25 -svn:wc:ra_dav:version-url -V 59 -/svn/!svn/ver/170/trunk/registration/management/__init__.py -END diff --git a/cas_provider/management/.svn/entries b/cas_provider/management/.svn/entries deleted file mode 100644 index 30ca011..0000000 --- a/cas_provider/management/.svn/entries +++ /dev/null @@ -1,65 +0,0 @@ -9 - -dir -170 -http://django-registration.googlecode.com/svn/trunk/registration/management -http://django-registration.googlecode.com/svn - - - -2008-09-22T10:00:14.397881Z -170 -ubernostrum - - -svn:special svn:externals svn:needs-lock - - - - - - - - - - - -b970a8a1-db28-0410-9b79-8b7e580363d3 - -commands -dir - -__init__.py -file - - - - -2009-02-18T02:12:14.000000Z -d41d8cd98f00b204e9800998ecf8427e -2008-09-22T10:00:14.397881Z -170 -ubernostrum - - - - - - - - - - - - - - - - - - - - - -0 - diff --git a/cas_provider/management/.svn/format b/cas_provider/management/.svn/format deleted file mode 100644 index ec63514..0000000 --- a/cas_provider/management/.svn/format +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/cas_provider/management/.svn/text-base/__init__.py.svn-base b/cas_provider/management/.svn/text-base/__init__.py.svn-base deleted file mode 100644 index e69de29..0000000 diff --git a/cas_provider/management/commands/cleanuptickets.py b/cas_provider/management/commands/cleanuptickets.py index 401bec1..994c766 100644 --- a/cas_provider/management/commands/cleanuptickets.py +++ b/cas_provider/management/commands/cleanuptickets.py @@ -6,6 +6,7 @@ Calls ``ServiceTickets.objects.delete_expired_users()``, which contains the actual logic for determining which accounts are deleted. """ +from __future__ import print_function, unicode_literals from django.core.management.base import NoArgsCommand from django.conf import settings @@ -18,21 +19,21 @@ class Command(NoArgsCommand): help = "Delete expired service tickets from the database" def handle_noargs(self, **options): - print "Service tickets:" + print("Service tickets:") tickets = ServiceTicket.objects.all() for ticket in tickets: expiration = datetime.timedelta(minutes=settings.CAS_TICKET_EXPIRATION) if datetime.datetime.now() > ticket.created + expiration: - print "Deleting %s..." % ticket.ticket + print("Deleting %s..." % ticket.ticket) ticket.delete() else: - print "%s not expired..." % ticket.ticket + print("%s not expired..." % ticket.ticket) tickets = LoginTicket.objects.all() - print "Login tickets:" + print("Login tickets:") for ticket in tickets: expiration = datetime.timedelta(minutes=settings.CAS_TICKET_EXPIRATION) if datetime.datetime.now() > ticket.created + expiration: - print "Deleting %s..." % ticket.ticket + print("Deleting %s..." % ticket.ticket) ticket.delete() else: - print "%s not expired..." % ticket.ticket \ No newline at end of file + print("%s not expired..." % ticket.ticket) diff --git a/cas_provider/migrations/0001_initial.py b/cas_provider/migrations/0001_initial.py index 74770af..6622ec5 100644 --- a/cas_provider/migrations/0001_initial.py +++ b/cas_provider/migrations/0001_initial.py @@ -1,92 +1,94 @@ -# encoding: utf-8 -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models +# -*- coding: utf-8 -*- +from __future__ import unicode_literals -class Migration(SchemaMigration): +from django.db import models, migrations +from django.conf import settings - 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']) +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] - 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'] + operations = [ + migrations.CreateModel( + name='LoginTicket', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('ticket', models.CharField(max_length=32, verbose_name='ticket')), + ('created', models.DateTimeField(auto_now=True, verbose_name='created')), + ], + options={ + 'verbose_name': 'Login Ticket', + 'verbose_name_plural': 'Login Tickets', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ProxyGrantingTicket', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('ticket', models.CharField(max_length=32, verbose_name='ticket')), + ('created', models.DateTimeField(auto_now=True, verbose_name='created')), + ('pgtiou', models.CharField(max_length=256, verbose_name='PGTiou')), + ], + options={ + 'verbose_name': 'Proxy Granting Ticket', + 'verbose_name_plural': 'Proxy Granting Tickets', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ProxyGrantingTicketIOU', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('ticket', models.CharField(max_length=32, verbose_name='ticket')), + ('created', models.DateTimeField(auto_now=True, verbose_name='created')), + ('proxyGrantingTicket', models.ForeignKey(verbose_name='Proxy Granting Ticket', to='cas_provider.ProxyGrantingTicket')), + ], + options={ + 'verbose_name': 'Proxy Granting Ticket IOU', + 'verbose_name_plural': 'Proxy Granting Tickets IOU', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ServiceTicket', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('ticket', models.CharField(max_length=32, verbose_name='ticket')), + ('created', models.DateTimeField(auto_now=True, verbose_name='created')), + ('service', models.URLField(verbose_name='service')), + ], + options={ + 'verbose_name': 'Service Ticket', + 'verbose_name_plural': 'Service Tickets', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ProxyTicket', + fields=[ + ('serviceticket_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cas_provider.ServiceTicket')), + ('proxyGrantingTicket', models.ForeignKey(verbose_name='Proxy Granting Ticket', to='cas_provider.ProxyGrantingTicket')), + ], + options={ + 'verbose_name': 'Proxy Ticket', + 'verbose_name_plural': 'Proxy Tickets', + }, + bases=('cas_provider.serviceticket',), + ), + migrations.AddField( + model_name='serviceticket', + name='user', + field=models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL), + preserve_default=True, + ), + migrations.AddField( + model_name='proxygrantingticket', + name='serviceTicket', + field=models.ForeignKey(to='cas_provider.ServiceTicket', null=True), + preserve_default=True, + ), + ] diff --git a/cas_provider/migrations/0002_auto_20140920_1644.py b/cas_provider/migrations/0002_auto_20140920_1644.py new file mode 100644 index 0000000..5948a66 --- /dev/null +++ b/cas_provider/migrations/0002_auto_20140920_1644.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('cas_provider', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='proxygrantingticket', + name='serviceTicket', + ), + migrations.AddField( + model_name='proxygrantingticket', + name='pgt', + field=models.ForeignKey(to='cas_provider.ProxyGrantingTicket', null=True), + preserve_default=True, + ), + migrations.AddField( + model_name='proxygrantingticket', + name='service', + field=models.URLField(null=True, verbose_name='service'), + preserve_default=True, + ), + migrations.AddField( + model_name='proxygrantingticket', + name='user', + field=models.ForeignKey(default=0, verbose_name='user', to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] 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 deleted file mode 100644 index 46247aa..0000000 --- a/cas_provider/migrations/0002_auto__add_proxygrantingticket__add_proxyticket__add_proxygrantingticke.py +++ /dev/null @@ -1,125 +0,0 @@ -# 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 deleted file mode 100644 index 46b5124..0000000 --- a/cas_provider/migrations/0003_auto__del_field_proxygrantingticket_targetService.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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/models.py b/cas_provider/models.py index 8306d3c..36014b5 100644 --- a/cas_provider/models.py +++ b/cas_provider/models.py @@ -1,16 +1,16 @@ -from django.contrib.auth.models import User +from __future__ import unicode_literals + +from django.conf import settings 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 +try: + from urllib.parse import urlencode, urlparse, parse_qs, ParseResult +except ImportError: + from urllib import urlencode + from urlparse import urlparse, ParseResult + from urlparse import parse_qs __all__ = ['ServiceTicket', 'LoginTicket', 'ProxyGrantingTicket', 'ProxyTicket', 'ProxyGrantingTicketIOU'] @@ -31,12 +31,12 @@ class BaseTicket(models.Model): 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)))) + return "%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) + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user')) + service = models.URLField(_('service')) prefix = 'ST' @@ -45,13 +45,13 @@ class ServiceTicket(BaseTicket): verbose_name_plural = _('Service Tickets') def get_redirect_url(self): - parsed = urlparse.urlparse(self.service) + parsed = 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, + query = [((k, v) if len(v) > 1 else (k, v[0])) for k, v in query.items()] + parsed = ParseResult(parsed.scheme, parsed.netloc, parsed.path, parsed.params, - urllib.urlencode(query), parsed.fragment) + urlencode(query), parsed.fragment) return parsed.geturl() @@ -64,13 +64,15 @@ class LoginTicket(BaseTicket): class ProxyGrantingTicket(BaseTicket): - serviceTicket = models.ForeignKey(ServiceTicket, null=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user')) + service = models.URLField(_('service'), null=True) + pgt = models.ForeignKey('self', 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))) + kwargs['pgtiou'] = "PGTIOU-%s" % (''.join(Random().sample(string.ascii_letters + string.digits, 50))) super(ProxyGrantingTicket, self).__init__(*args, **kwargs) class Meta: diff --git a/cas_provider/signals.py b/cas_provider/signals.py index ad366fd..3c469db 100644 --- a/cas_provider/signals.py +++ b/cas_provider/signals.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """cas_provider.signals -- signal definitions for cas_provider """ +from __future__ import unicode_literals + from django import dispatch diff --git a/cas_provider/templates/cas/login.html b/cas_provider/templates/cas/login.html index d9e933e..ea1377e 100644 --- a/cas_provider/templates/cas/login.html +++ b/cas_provider/templates/cas/login.html @@ -1,11 +1,11 @@ -{% extends "base.html" %} +{% extends "admin/base.html" %} {% block title %} Login {% endblock %} {% block content %} -
+
Log in to your account {% csrf_token %} diff --git a/cas_provider/templates/cas/logout.html b/cas_provider/templates/cas/logout.html index f7b25e9..e31c5a3 100644 --- a/cas_provider/templates/cas/logout.html +++ b/cas_provider/templates/cas/logout.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "admin/base.html" %} {% block title %} Logged out diff --git a/cas_provider/templates/cas/warn.html b/cas_provider/templates/cas/warn.html index ca9e561..9c74eee 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 deleted file mode 100644 index 46ba3ca..0000000 --- a/cas_provider/tests.py +++ /dev/null @@ -1,351 +0,0 @@ -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 9167b1e..fb59599 100644 --- a/cas_provider/urls.py +++ b/cas_provider/urls.py @@ -1,4 +1,6 @@ -from django.conf.urls.defaults import patterns, url +from __future__ import unicode_literals + +from django.conf.urls import patterns, url urlpatterns = patterns('cas_provider.views', diff --git a/cas_provider/views.py b/cas_provider/views.py index 7aeaafb..76bcbb2 100644 --- a/cas_provider/views.py +++ b/cas_provider/views.py @@ -1,11 +1,16 @@ +from __future__ import unicode_literals + import logging logger = logging.getLogger('cas_provider.views') -import urllib -import logging -from urllib import urlencode -import urllib2 -import urlparse +try: + from urllib.error import HTTPError, URLError + from urllib.parse import parse_qsl, urlencode, urlparse, urlsplit, urlunsplit + from urllib.request import urlopen +except ImportError: + from urllib import urlencode + from urllib2 import HTTPError, URLError, urlopen + from urlparse import parse_qsl, urlparse, urlsplit, urlunsplit from functools import wraps from django.utils.decorators import available_attrs @@ -19,16 +24,17 @@ 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.utils.translation import ugettext as _ from django.template import RequestContext from django.contrib.auth import authenticate from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ 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 @@ -41,10 +47,10 @@ 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'), + (INVALID_TICKET, 'The provided ticket is invalid.'), + (INVALID_SERVICE, 'Service is invalid'), + (INVALID_REQUEST, 'Not all required parameters were sent.'), + (INTERNAL_ERROR, 'An internal error occurred during ticket validation'), ) @@ -98,7 +104,7 @@ def login(request, template_name='cas/login.html', if form.is_valid(): service = form.cleaned_data.get('service', None) try: - auth_args = dict(username=form.cleaned_data['email'], + auth_args = dict(username=form.cleaned_data['username'], password=form.cleaned_data['password']) if merge: # We only want to send the merge argument if it's @@ -106,7 +112,7 @@ def login(request, template_name='cas/login.html', # through the auth backends properly. auth_args['merge'] = merge user = authenticate(**auth_args) - except SameEmailMismatchedPasswords: + except: # Need to merge the accounts? if merge: # We shouldn't get here... @@ -115,32 +121,32 @@ def login(request, template_name='cas/login.html', base_url = reverse('cas_provider_merge') args = dict( success_redirect=success_redirect, - email=form.cleaned_data['email'], + username=form.cleaned_data['username'], ) if service is not None: args['service'] = service - args = urllib.urlencode(args) + args = urlencode(args) url = '%s?%s' % (base_url, args) logging.debug('Redirecting to %s', url) return HttpResponseRedirect(url) if user is None: - errors.append('Incorrect username and/or password.') + errors.append(_('Incorrect username and/or password.')) else: if user.is_active: auth_login(request, user) else: # Not a POST... if merge: - form = MergeLoginForm(initial={'service': service, 'email': request.GET.get('email')}) + form = MergeLoginForm(initial={'service': service, 'username': request.GET.get('username')}) else: form = LoginForm(initial={'service': service}) if user is not None and user.is_authenticated(): # We have an authenticated user. if not user.is_active: - errors.append('This account is disabled.') + errors.append(_('This account is disabled. Please contact us if you feel it should be enabled again.')) else: # Send the on_cas_login signal. If we get an HttpResponse, return that. for receiver, response in signals.on_cas_login.send(sender=login, request=request, **kwargs): @@ -235,7 +241,7 @@ def proxy(request): return _cas2_error_response(INVALID_TICKET) pt = ProxyTicket.objects.create(proxyGrantingTicket=proxyGrantingTicket, - user=proxyGrantingTicket.serviceTicket.user, + user=proxyGrantingTicket.user, service=targetService) return _cas2_proxy_success(pt.ticket) @@ -256,8 +262,8 @@ def ticket_validate(service, ticket_string, pgtUrl): except ServiceTicket.DoesNotExist: return _cas2_error_response(INVALID_TICKET) - ticketUrl = urlparse.urlparse(ticket.service) - serviceUrl = urlparse.urlparse(service) + ticketUrl = urlparse(ticket.service) + serviceUrl = urlparse(service) if not(ticketUrl.hostname == serviceUrl.hostname and ticketUrl.path == serviceUrl.path and ticketUrl.port == serviceUrl.port): return _cas2_error_response(INVALID_SERVICE) @@ -269,16 +275,16 @@ def ticket_validate(service, ticket_string, pgtUrl): if pgt: pgtIouId = pgt.pgtiou - if hasattr(ticket, 'proxyticket'): - pgt = ticket.proxyticket.proxyGrantingTicket + try: + proxyTicket = ticket.proxyticket + except ProxyTicket.DoesNotExist: + pass + else: + pgt = 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 + while pgt.pgt is not None: + proxies += (pgt.service,) + pgt = pgt.pgt user = ticket.user ticket.delete() @@ -308,31 +314,29 @@ def proxy_validate(request): def generate_proxy_granting_ticket(pgt_url, ticket): proxy_callback_good_status = (200, 202, 301, 302, 304) - uri = list(urlparse.urlsplit(pgt_url)) + uri = list(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 + pgt.user = ticket.user + pgt.service = ticket.service + # Remember if it's a chained PGT. + pgt.pgt = getattr(ticket, 'proxyGrantingTicket', None) params = {'pgtId': pgt.ticket, 'pgtIou': pgt.pgtiou} - query = dict(urlparse.parse_qsl(uri[4])) + query = dict(parse_qsl(uri[4])) query.update(params) uri[3] = urlencode(query) try: - response = urllib2.urlopen(urlparse.urlunsplit(uri)) - except urllib2.HTTPError as e: + urlopen(urlunsplit(uri)) + except 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)) + logger.debug('Checking Proxy Callback URL {0} returned {1}. 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)) + except URLError as e: + logger.debug('Checking Proxy Callback URL {0} raised URLError. Not issuing PGT.'.format(uri)) return pgt.save() @@ -344,18 +348,18 @@ def _cas2_proxy_success(pt): def _cas2_sucess_response(user, pgt=None, proxies=None): - return HttpResponse(auth_success_response(user, pgt, proxies), mimetype='text/xml') + return HttpResponse(auth_success_response(user, pgt, proxies), content_type='text/xml') def _cas2_error_response(code, message=None): - return HttpResponse(u''' + return HttpResponse(''' %(message)s ''' % { 'code': code, 'message': message if message else dict(ERROR_MESSAGES).get(code) - }, mimetype='text/xml') + }, content_type='text/xml') def proxy_success(pt): @@ -363,7 +367,7 @@ def proxy_success(pt): 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') + return etree.tostring(response, encoding='unicode') def auth_success_response(user, pgt, proxies): @@ -398,4 +402,4 @@ def auth_success_response(user, pgt, proxies): proxyElement = etree.SubElement(proxiesElement, CAS + "proxy") proxyElement.text = proxy - return unicode(etree.tostring(response, encoding='utf-8'), 'utf-8') + return etree.tostring(response, encoding='unicode') diff --git a/cas_provider_examples/manage.py b/cas_provider_examples/manage.py new file mode 100755 index 0000000..a2aa65d --- /dev/null +++ b/cas_provider_examples/manage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +from __future__ import unicode_literals + +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "simple.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/cas_provider_examples/simple/manage.py b/cas_provider_examples/simple/manage.py deleted file mode 100644 index 3e4eedc..0000000 --- a/cas_provider_examples/simple/manage.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/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/models.py b/cas_provider_examples/simple/models.py new file mode 100644 index 0000000..e69de29 diff --git a/cas_provider_examples/simple/settings.py b/cas_provider_examples/simple/settings.py index 209927c..51a1436 100644 --- a/cas_provider_examples/simple/settings.py +++ b/cas_provider_examples/simple/settings.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + # Django settings for xxx project. DEBUG = True @@ -104,10 +106,11 @@ import os PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) TEMPLATE_DIRS = ( - os.path.join(PROJECT_PATH, 'templates') + os.path.join(PROJECT_PATH, 'templates'), ) INSTALLED_APPS = ( + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -115,7 +118,7 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'cas_provider', - 'south', + 'simple', ) # A sample logging configuration. The only tangible logging diff --git a/cas_provider_examples/simple/tests.py b/cas_provider_examples/simple/tests.py new file mode 100644 index 0000000..2d95228 --- /dev/null +++ b/cas_provider_examples/simple/tests.py @@ -0,0 +1,381 @@ +from __future__ import unicode_literals +from io import StringIO +from xml import etree +from xml.etree import ElementTree +import cas_provider +from cas_provider.attribute_formatters import CAS +from cas_provider.models import ServiceTicket +from cas_provider.signals import cas_collect_custom_attributes +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 django.conf import settings + +try: + from urllib.parse import urlparse, parse_qsl, parse_qs + from urllib.request import install_opener +except: + from urlparse import urlparse, parse_qsl, parse_qs + from urllib2 import install_opener + + + + +class DummyOpener(object): + url = None + + @staticmethod + def open(url, *args, **kwargs): + DummyOpener.url = url + + +class ViewsTest(TestCase): + + fixtures = ['cas_users', ] + + + + def setUp(self): + self.service = 'http://example.com/' + + + def test_successful_login_with_proxy(self): + install_opener(DummyOpener) # Don't really open any URLs + 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(response.content.decode('utf-8'))) + auth_success = responseXml.find(CAS + 'authenticationSuccess') + pgt = auth_success.find(CAS + "proxyGrantingTicket") + user = auth_success.find(CAS + "user") + self.assertEqual('root', user.text) + self.assertIsNotNone(pgt.text) + self.assertTrue(pgt.text.startswith('PGTIOU')) + + pgtId = parse_qs(urlparse(DummyOpener.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(proxyTicketResponse.content.decode('utf-8'))) + self.assertIsNotNone(proxyTicketResponseXml.find(CAS + "proxySuccess")) + self.assertIsNotNone(proxyTicketResponseXml.find(CAS + "proxySuccess/" + CAS + "proxyTicket")) + proxyTicket = proxyTicketResponseXml.find(CAS + "proxySuccess/" + CAS + "proxyTicket"); + + #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(proxyValidateResponse.content.decode('utf-8'))) + + auth_success_2 = proxyValidateResponseXml.find(CAS + 'authenticationSuccess') + user_2 = auth_success.find(CAS + "user") + 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): + install_opener(DummyOpener) # Don't really open any URLs + 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(response.content.decode('utf-8'))) + auth_success_1 = responseXml.find(CAS + 'authenticationSuccess') + pgt_1 = auth_success_1.find(CAS + "proxyGrantingTicket") + user_1 = auth_success_1.find(CAS + "user") + self.assertEqual('root', user_1.text) + self.assertIsNotNone(pgt_1.text) + self.assertTrue(pgt_1.text.startswith('PGTIOU')) + + pgtId_1 = parse_qs(urlparse(DummyOpener.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(proxyTicketResponse_1.content.decode('utf-8'))) + self.assertIsNotNone(proxyTicketResponseXml_1.find(CAS + "proxySuccess")) + self.assertIsNotNone(proxyTicketResponseXml_1.find(CAS + "proxySuccess/" + CAS + "proxyTicket")) + proxyTicket_1 = proxyTicketResponseXml_1.find(CAS + "proxySuccess/" + CAS +"proxyTicket"); + + #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(proxyValidateResponse_1.content.decode('utf-8'))) + + auth_success_2 = proxyValidateResponseXml_1.find(CAS + 'authenticationSuccess') + user_2 = auth_success_2.find(CAS + "user") + + 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") + user = auth_success_2.find(CAS + "user") + self.assertEqual('root', user.text) + self.assertIsNotNone(pgt_2.text) + self.assertTrue(pgt_2.text.startswith('PGTIOU')) + + pgtId_2 = parse_qs(urlparse(DummyOpener.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(proxyTicketResponse_2.content.decode('utf-8'))) + self.assertIsNotNone(proxyTicketResponseXml_2.find(CAS + "proxySuccess")) + self.assertIsNotNone(proxyTicketResponseXml_2.find(CAS + "proxySuccess/" + CAS + "proxyTicket")) + proxyTicket_2 = proxyTicketResponseXml_2.find(CAS + "proxySuccess/" + CAS + "proxyTicket") + + proxyValidateResponse_3 = self.client.get(reverse('cas_proxy_validate'), {'ticket': proxyTicket_2.text, 'service': proxyTarget_2, 'pgtUrl': None}) + proxyValidateResponseXml_3 = ElementTree.parse(StringIO(proxyValidateResponse_3.content.decode('utf-8'))) + + auth_success_3 = proxyValidateResponseXml_3.find(CAS + 'authenticationSuccess') + user_3 = auth_success_3.find(CAS + "user") + + 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") + 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_validate_twice(self): + response = self._login_user('root', '123') + response2 = self._validate_cas2(response, True) + user = User.objects.get(username=self.username) + self.assertEqual(response2.content, _cas2_sucess_response(user).content) + + response2 = self._validate_cas2(response, True) + self.assertEqual(response2.content, _cas2_error_response(INVALID_TICKET).content) + + def test_cas2_validate_twice_with_pgt(self): + proxyTarget = "http://my.sweet.service_1" + response = self._login_user('root', '123') + response2 = self._validate_cas2(response, True, proxyTarget) + user = User.objects.get(username=self.username) + self.assertEqual(response2.content, _cas2_sucess_response(user).content) + + response2 = self._validate_cas2(response, True, proxyTarget) + self.assertEqual(response2.content, _cas2_error_response(INVALID_TICKET).content) + + def test_cas2_custom_attrs(self): + cas_collect_custom_attributes.connect(cas_mapping) + response = self._login_user('editor', '123') + self.maxDiff=None + + response = self._validate_cas2(response, True) + self.assertEqual(response.content.decode('utf-8'), + '''''' + '''''' + '''editor''' + '''''' + '''Jasig''' + '''editor@exapmle.com''' + '''editor''' + '''True''' + '''True''' + '''''' + '''''' + '''''') + + 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.decode('utf-8'), + '''''' + '''''' + '''editor''' + '''RubyCAS''' + '''editor@exapmle.com''' + '''editor''' + '''True''' + '''True''' + '''''' + '''''') + + 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.decode('utf-8'), + '''''' + '''''' + '''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): + install_opener(DummyOpener) # Don't really open any URLs + 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 = DummyOpener.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, + '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(response.content.decode('utf-8'), 'yes\n%s\n' % self.username) + else: + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['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.decode('utf-8'), '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['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(sender, user, **kwargs): + return { + 'is_staff': str(user.is_staff), + 'is_active': str(user.is_active), + 'email': user.email, + 'group': [g.name for g in user.groups.all()] + } diff --git a/cas_provider_examples/simple/urls.py b/cas_provider_examples/simple/urls.py index c717237..71e70f3 100644 --- a/cas_provider_examples/simple/urls.py +++ b/cas_provider_examples/simple/urls.py @@ -1,10 +1,12 @@ -from django.conf.urls.defaults import patterns, include, url +from __future__ import unicode_literals -import cas_provider -from django.views.generic.simple import redirect_to, direct_to_template +from django.conf.urls import patterns, include, url +from django.contrib import admin +from django.views.generic import TemplateView urlpatterns = patterns('', + url(r'^admin/', include(admin.site.urls)), url(r'^', include('cas_provider.urls')), - url(r'^accounts/profile', direct_to_template, {'template': 'login-success-redirect-target.html'}), + url(r'^accounts/profile', TemplateView.as_view(template_name='login-success-redirect-target.html')), ) diff --git a/setup.py b/setup.py index fe96615..40effde 100644 --- a/setup.py +++ b/setup.py @@ -11,13 +11,15 @@ setup( 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(), + packages=find_packages(exclude=['cas_provider_examples']), include_package_data=True, license='MIT', long_description=read('README.rst'), zip_safe=False, - install_requires=['setuptools', - 'south>=0.7.2',], + install_requires=[ + 'Django>=1.5,<1.8', + 'lxml', + ], classifiers = [ "Development Status :: 3 - Alpha", "Framework :: Django", diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c11d95d --- /dev/null +++ b/tox.ini @@ -0,0 +1,30 @@ +[tox] +envlist=clear, + d1{5,6}-py{26,27,32,33}, + d{17,d}-py{27,32,33,34}, + stats + +[testenv] +indexserver=https://py.mdrn.pl:8443 +commands=coverage run --source=cas_provider --append --branch cas_provider_examples/manage.py test simple +deps= + d15: Django>=1.5,<1.6 + d16: Django>=1.6,<1.7 + d17: Django>=1.7,<1.8 + dd: https://github.com/django/django/zipball/master + coverage +basepython= + py26: python2.6 + py27: python2.7 + py32: python3.2 + py33: python3.3 + py34: python3.4 + +[testenv:clear] +basepython=python3.4 +commands=coverage erase + +[testenv:stats] +basepython=python3.4 +commands=coverage html +