diff --git a/.gitignore b/.gitignore index f2b02b7..c05c31f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ *.swp build/ -bootstrap3 cas/ dist/ db.sqlite3 diff --git a/Makefile b/Makefile index 68fd801..c719834 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ test_venv/cas/manage.py: test_venv mkdir -p test_venv/cas test_venv/bin/django-admin startproject cas test_venv/cas ln -s ../../cas_server test_venv/cas/cas_server - sed -i "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n 'bootstrap3',\n 'cas_server',/" test_venv/cas/cas/settings.py + sed -i "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n 'cas_server',/" test_venv/cas/cas/settings.py sed -i "s/'django.middleware.clickjacking.XFrameOptionsMiddleware',/'django.middleware.clickjacking.XFrameOptionsMiddleware',\n 'django.middleware.locale.LocaleMiddleware',/" test_venv/cas/cas/settings.py sed -i 's/from django.conf.urls import url/from django.conf.urls import url, include/' test_venv/cas/cas/urls.py sed -i "s@url(r'^admin/', admin.site.urls),@url(r'^admin/', admin.site.urls),\n url(r'^', include('cas_server.urls', namespace='cas_server')),@" test_venv/cas/cas/urls.py diff --git a/README.rst b/README.rst index 90001c8..8675112 100644 --- a/README.rst +++ b/README.rst @@ -9,13 +9,6 @@ CAS Server is a Django application implementing the `CAS Protocol 3.0 Specificat By default, the authentication process use django internal users but you can easily use any sources (see auth classes in the auth.py file) -The default login/logout template use `django-bootstrap3 `__ -but you can use your own templates using settings variables. - -Note that for Django 1.7 compatibility, you need a version of -`django-bootstrap3 `__ < 7.0.0 -like the 6.2.2 version. - .. contents:: Table of Contents Features @@ -39,8 +32,6 @@ Dependencies * Django >= 1.7 < 1.10 * requests >= 2.4 * requests_futures >= 0.9.5 -* django-picklefield >= 0.3.1 -* django-bootstrap3 >= 5.4 (< 7.0.0 if using django 1.7) * lxml >= 3.4 * six >= 1 @@ -55,7 +46,7 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa On debian like systems:: - $ sudo apt-get install python-django python-requests python-django-picklefield python-six python-lxml + $ sudo apt-get install python-django python-requests python-six python-lxml python-requests-futures On debian jessie, you can use the version of python-django available in the `backports `_. @@ -105,7 +96,6 @@ Quick start INSTALLED_APPS = ( 'django.contrib.admin', ... - 'bootstrap3', 'cas_server', ) @@ -173,6 +163,17 @@ Template settings * ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default templates. Set it to ``False`` to disable it. +* ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary + and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``, + ``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is:: + + { + "bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", + "bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js", + "html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js", + "respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js", + "jquery": "//code.jquery.com/jquery.min.js", + } * ``CAS_LOGIN_TEMPLATE``: Path to the template showed on ``/login`` then the user is not autenticated. The default is ``"cas_server/login.html"``. diff --git a/cas_server/default_settings.py b/cas_server/default_settings.py index 3c08af3..c7b2b12 100644 --- a/cas_server/default_settings.py +++ b/cas_server/default_settings.py @@ -18,6 +18,14 @@ from importlib import import_module #: URL to the logo showed in the up left corner on the default templates. CAS_LOGO_URL = static("cas_server/logo.png") +#: URLs to css and javascript external components. +CAS_COMPONENT_URLS = { + "bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", + "bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js", + "html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js", + "respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js", + "jquery": "//code.jquery.com/jquery.min.js", +} #: Path to the template showed on /login then the user is not autenticated. CAS_LOGIN_TEMPLATE = 'cas_server/login.html' #: Path to the template showed on /login?service=... then the user is authenticated and has asked diff --git a/cas_server/federate.py b/cas_server/federate.py index 156d3ea..2cfd90e 100644 --- a/cas_server/federate.py +++ b/cas_server/federate.py @@ -84,7 +84,7 @@ class CASFederateValidateUser(object): if username is not None: if attributs is None: attributs = {} - attributs["provider"] = self.provider + attributs["provider"] = self.provider.suffix self.username = username self.attributs = attributs user = FederatedUser.objects.update_or_create( diff --git a/cas_server/forms.py b/cas_server/forms.py index 4b35008..03c7515 100644 --- a/cas_server/forms.py +++ b/cas_server/forms.py @@ -18,7 +18,28 @@ import cas_server.utils as utils import cas_server.models as models -class WarnForm(forms.Form): +class BootsrapForm(forms.Form): + """Form base class to use boostrap then rendering the form fields""" + def __init__(self, *args, **kwargs): + super(BootsrapForm, self).__init__(*args, **kwargs) + for (name, field) in self.fields.items(): + # Only tweak the fiel if it will be displayed + if not isinstance(field.widget, forms.HiddenInput): + # tell to display the field (used in form.html) + self[name].display = True + attrs = {} + if isinstance(field.widget, forms.CheckboxInput): + self[name].checkbox = True + else: + attrs['class'] = "form-control" + if field.label: + attrs["placeholder"] = field.label + if field.required: + attrs["required"] = "required" + field.widget.attrs.update(attrs) + + +class WarnForm(BootsrapForm): """ Bases: :class:`django.forms.Form` @@ -38,7 +59,7 @@ class WarnForm(forms.Form): lt = forms.CharField(widget=forms.HiddenInput(), required=False) -class FederateSelect(forms.Form): +class FederateSelect(BootsrapForm): """ Bases: :class:`django.forms.Form` @@ -66,7 +87,7 @@ class FederateSelect(forms.Form): renew = forms.BooleanField(widget=forms.HiddenInput(), required=False) -class UserCredential(forms.Form): +class UserCredential(BootsrapForm): """ Bases: :class:`django.forms.Form` diff --git a/cas_server/migrations/0001_squashed_0021_auto_20150611_2102.py b/cas_server/migrations/0001_squashed_0021_auto_20150611_2102.py index a2000bc..c3d3785 100644 --- a/cas_server/migrations/0001_squashed_0021_auto_20150611_2102.py +++ b/cas_server/migrations/0001_squashed_0021_auto_20150611_2102.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from django.db import models, migrations import django.db.models.deletion import cas_server.utils -import picklefield.fields class Migration(migrations.Migration): @@ -31,7 +30,7 @@ class Migration(migrations.Migration): name='ProxyGrantingTicket', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('attributs', picklefield.fields.PickledObjectField(editable=False)), + ('attributs', models.TextField(blank=True, default=None, null=True)), ('validate', models.BooleanField(default=False)), ('service', models.TextField()), ('creation', models.DateTimeField(auto_now_add=True)), @@ -47,7 +46,7 @@ class Migration(migrations.Migration): name='ProxyTicket', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('attributs', picklefield.fields.PickledObjectField(editable=False)), + ('attributs', models.TextField(blank=True, default=None, null=True)), ('validate', models.BooleanField(default=False)), ('service', models.TextField()), ('creation', models.DateTimeField(auto_now_add=True)), @@ -80,7 +79,7 @@ class Migration(migrations.Migration): name='ServiceTicket', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('attributs', picklefield.fields.PickledObjectField(editable=False)), + ('attributs', models.TextField(blank=True, default=None, null=True)), ('validate', models.BooleanField(default=False)), ('service', models.TextField()), ('creation', models.DateTimeField(auto_now_add=True)), diff --git a/cas_server/migrations/0005_auto_20160616_1018.py b/cas_server/migrations/0005_auto_20160616_1018.py index fea9167..8d361b9 100644 --- a/cas_server/migrations/0005_auto_20160616_1018.py +++ b/cas_server/migrations/0005_auto_20160616_1018.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from django.db import migrations, models -import picklefield.fields import django.db.models.deletion @@ -41,7 +40,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(max_length=124)), ('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cas_server.FederatedIendityProvider')), - ('attributs', picklefield.fields.PickledObjectField(editable=False)), + ('attributs', models.TextField(blank=True, default=None, null=True)), ('ticket', models.CharField(max_length=255)), ('last_update', models.DateTimeField(auto_now=True)), ], diff --git a/cas_server/migrations/0007_auto_20160723_2252.py b/cas_server/migrations/0007_auto_20160723_2252.py new file mode 100644 index 0000000..fd0c8a1 --- /dev/null +++ b/cas_server/migrations/0007_auto_20160723_2252.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-07-23 22:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0006_auto_20160706_1727'), + ] + + operations = [ + migrations.RemoveField( + model_name='federateduser', + name='attributs', + ), + migrations.RemoveField( + model_name='proxygrantingticket', + name='attributs', + ), + migrations.RemoveField( + model_name='proxyticket', + name='attributs', + ), + migrations.RemoveField( + model_name='serviceticket', + name='attributs', + ), + migrations.AddField( + model_name='federateduser', + name='_attributs', + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='proxygrantingticket', + name='_attributs', + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='proxyticket', + name='_attributs', + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='serviceticket', + name='_attributs', + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AlterField( + model_name='federatediendityprovider', + name='suffix', + field=models.CharField(help_text='Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``.', max_length=30, unique=True, verbose_name='suffix'), + ), + ] diff --git a/cas_server/models.py b/cas_server/models.py index fbfd1e4..6e87d40 100644 --- a/cas_server/models.py +++ b/cas_server/models.py @@ -18,7 +18,6 @@ from django.contrib import messages from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible -from picklefield.fields import PickledObjectField import re import sys @@ -140,8 +139,8 @@ class FederatedUser(models.Model): username = models.CharField(max_length=124) #: A foreign key to :class:`FederatedIendityProvider` provider = models.ForeignKey(FederatedIendityProvider, on_delete=models.CASCADE) - #: The user attributes returned by the CAS backend on successful ticket validation - attributs = PickledObjectField() + #: The user attributes json encoded + _attributs = models.TextField(default=None, null=True, blank=True) #: The last ticket used to authenticate :attr:`username` against :attr:`provider` ticket = models.CharField(max_length=255) #: Last update timespampt. Usually, the last time :attr:`ticket` has been set. @@ -150,6 +149,17 @@ class FederatedUser(models.Model): def __str__(self): return self.federated_username + @property + def attributs(self): + """The user attributes returned by the CAS backend on successful ticket validation""" + if self._attributs is not None: + return utils.json.loads(self._attributs) + + @attributs.setter + def attributs(self, value): + """attributs property setter""" + self._attributs = utils.json_encode(value) + @property def federated_username(self): """The federated username with a suffix for the current :class:`FederatedUser`.""" @@ -712,8 +722,8 @@ class Ticket(models.Model): abstract = True #: ForeignKey to a :class:`User`. user = models.ForeignKey(User, related_name="%(class)s") - #: The user attributes to be transmited to the service on successful validation - attributs = PickledObjectField() + #: The user attributes to transmit to the service json encoded + _attributs = models.TextField(default=None, null=True, blank=True) #: A boolean. ``True`` if the ticket has been validated validate = models.BooleanField(default=False) #: The service url for the ticket @@ -736,6 +746,17 @@ class Ticket(models.Model): #: requests. TIMEOUT = settings.CAS_TICKET_TIMEOUT + @property + def attributs(self): + """The user attributes to be transmited to the service on successful validation""" + if self._attributs is not None: + return utils.json.loads(self._attributs) + + @attributs.setter + def attributs(self, value): + """attributs property setter""" + self._attributs = utils.json_encode(value) + class DoesNotExist(Exception): """raised in :meth:`Ticket.get` then ticket prefix and ticket classes mismatch""" pass diff --git a/cas_server/templates/cas_server/base.html b/cas_server/templates/cas_server/base.html index bebf439..db61e1b 100644 --- a/cas_server/templates/cas_server/base.html +++ b/cas_server/templates/cas_server/base.html @@ -1,36 +1,63 @@ -{% extends 'bootstrap3/bootstrap3.html' %} {% load i18n %} -{% block bootstrap3_title %}{% block title %}{% trans "Central Authentication Service" %}{% endblock %}{% endblock %} - {% load staticfiles %} -{% load bootstrap3 %} - -{% block bootstrap3_extra_head %} - - -{% endblock %} - -{% block bootstrap3_content %} -
-{% if auto_submit %}{% endif %} -
-
-
-{% if auto_submit %}{% endif %} -{% block content %} -{% endblock %} -
-
-
-
-{% endblock %} + + + + + + + {% block title %}{% trans "Central Authentication Service" %}{% endblock %} + + + + + + + + +
+ {% if auto_submit %}{% endif %} +
+
+
+ {% block ante_messages %}{% endblock %} + {% if auto_submit %}{% endif %} + {% block content %}{% endblock %} +
+
+
+
+ + + + diff --git a/cas_server/templates/cas_server/form.html b/cas_server/templates/cas_server/form.html new file mode 100644 index 0000000..5ac1463 --- /dev/null +++ b/cas_server/templates/cas_server/form.html @@ -0,0 +1,25 @@ +{% for error in form.non_field_errors %} +
+ + {{error}} +
+{% endfor %} +{% for field in form %}{% if field.display %} +
{% spaceless %} + {% if field.checkbox %} +
+ {% else %} + + {{field}} + {% endif %} + {% for error in field.errors %} + {{error}} + {% endfor %} +{% endspaceless %}
+{% else %}{{field}}{% endif %}{% endfor %} diff --git a/cas_server/templates/cas_server/logged.html b/cas_server/templates/cas_server/logged.html index 9c8bb38..f29445b 100644 --- a/cas_server/templates/cas_server/logged.html +++ b/cas_server/templates/cas_server/logged.html @@ -1,6 +1,4 @@ {% extends "cas_server/base.html" %} -{% load bootstrap3 %} -{% load staticfiles %} {% load i18n %} {% block content %} @@ -10,7 +8,7 @@ {% trans "Log me out from all my sessions" %}
- {% bootstrap_button _('Logout') size='lg' button_type="submit" button_class="btn-danger btn-block"%} + {% endblock %} diff --git a/cas_server/templates/cas_server/login.html b/cas_server/templates/cas_server/login.html index d4559fe..d6adc64 100644 --- a/cas_server/templates/cas_server/login.html +++ b/cas_server/templates/cas_server/login.html @@ -1,18 +1,19 @@ {% extends "cas_server/base.html" %} -{% load bootstrap3 %} -{% load staticfiles %} {% load i18n %} + +{% block ante_messages %} +{% if auto_submit %}{% endif %} +{% endblock %} {% block content %} - + {% if auto_submit %}