--- /dev/null
+================================
+Fundacja Nowoczesna Polska - CAS
+================================
+
+O projekcie
+===========
+CAS to aplikacja WWW służąca do autentykacji (a w przyszłości również autoryzacji) użytkowników
+serwisów Fundacji Nowoczesna Polska. Implementuje on protokół `CAS <http://www.jasig.org/cas>`_ w
+wersji 1.0.
+
+Wymagania
+=========
+* `Django 1.1 <http://djangoproject.com/>`_
+* `zuber/django-cas-provider <http://github.com/zuber/django-cas-provider>`_
+
+Instalacja i uruchomienie
+=========================
+1. Ściągnij i zainstaluj `pip <http://pypi.python.org/pypi/pip>`_
+2. Przejdź do katalogu aplikacji w konsoli
+3. Zainstaluj wymagane biblioteki (patrz sekcja wymagania_) komendą::
+
+ pip install -r requirements.txt
+
+4. Wypełnij bazę danych (Django poprosi o utworzenie pierwszego użytkownika)::
+
+ ./manage.py syncdb
+
+5. Uruchom serwer deweloperski::
+
+ ./manage.py runserver
+
+6. Przy wdrożeniu będziesz musiał najpewniej utworzyć plik `localsettings.py` i wpisać tam
+ustawienia używanej bazy danych. Zalecane jest serwowanie aplikacji
+przez `modwsgi <http://code.google.com/p/modwsgi/>`_ na serwerze `Apache2 <http://httpd.apache.org/>`_
+przy pomocy załączonego skryptu `dispatch.fcgi`. Inne strategie wdrożeniowe opisane
+są w `Dokumentacji Django <http://docs.djangoproject.com/en/dev/howto/deployment/#howto-deployment-index>`_.
\ No newline at end of file
--- /dev/null
+#!%(python)s
+import site
+site.addsitedir('%(path)s/lib/python2.5/site-packages')
+
+import os
+from os.path import abspath, dirname, join
+import sys
+
+# Redirect sys.stdout to sys.stderr for bad libraries like geopy that use
+# print statements for optional import exceptions.
+sys.stdout = sys.stderr
+
+# Add apps and lib directories to PYTHONPATH
+sys.path = [
+ '%(path)s/releases/current/%(project_name)s',
+ '%(path)s/releases/current/provider',
+ '%(path)s/releases/current',
+] + sys.path
+
+# Run Django
+os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
+
+from django.core.handlers.wsgi import WSGIHandler
+application = WSGIHandler()
--- /dev/null
+[loggers]
+keys=root,fnp
+
+[handlers]
+keys=console,lf
+
+[formatters]
+keys=default
+
+[logger_root]
+level=DEBUG
+handlers=console
+
+[logger_fnp]
+level=DEBUG
+handlers=lf,console
+qualname=fnp
+propagate=0
+
+[formatter_default]
+format=%(asctime)s %(name)s/%(levelname)s :: %(module)s:%(lineno)d :: %(message)s
+datefmt=
+
+[handler_console]
+class=StreamHandler
+level=DEBUG
+formatter=default
+args=(sys.stderr, )
+
+[handler_lf]
+class=FileHandler
+level=DEBUG
+formatter=default
+args=("/var/services/logs/cas.log",)
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env python
+from django.core.management import execute_manager
+
+from os import path
+import sys
+
+PROJECT_ROOT = path.realpath(path.dirname(__file__))
+sys.path.insert(0, path.abspath(path.join(PROJECT_ROOT, "..", "provider")))
+print sys.path
+
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import traceback
+ traceback.print_exc(file =sys.stderr)
+ 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(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ # Append lib and apps directories to PYTHONPATH
+
+
+ execute_manager(settings)
--- /dev/null
+# -*- coding: utf-8 -*-
+from os import path
+
+PROJECT_ROOT = path.realpath(path.dirname(__file__))
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = []
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+DATABASE_NAME = PROJECT_ROOT + '/dev.sqlite' # Or path to database file if using sqlite3.
+DATABASE_USER = '' # Not used with sqlite3.
+DATABASE_PASSWORD = '' # Not used with sqlite3.
+DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'Europe/Warsaw'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'pl'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = PROJECT_ROOT + '/media/'
+STATIC_ROOT = PROJECT_ROOT + '/static/'
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = '/media/'
+STATIC_URL = '/static/'
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/admin-media/'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.load_template_source',
+ 'django.template.loaders.app_directories.load_template_source',
+# 'django.template.loaders.eggs.load_template_source',
+)
+
+TEMPLATE_CONTEXT_PROCESSORS = (
+ "django.core.context_processors.auth",
+ "django.core.context_processors.debug",
+ "django.core.context_processors.i18n",
+ "django.core.context_processors.request",
+)
+
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.middleware.doc.XViewMiddleware',
+)
+
+ROOT_URLCONF = 'urls'
+
+TEMPLATE_DIRS = (
+ PROJECT_ROOT + '/templates',
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.admin',
+ 'django.contrib.admindocs',
+
+ 'cas_provider',
+)
+
+# django-cas-provider settings
+LOGIN_URL = '/cas/login/'
+LOGOUT_URL = '/cas/logout/'
+CAS_CUSTOM_ATTRIBUTES_CALLBACK = 'utils.custom_attributes_callback'
+SESSION_COOKIE_NAME = 'fnpcas_sessionid'
+
+# Python logging settings
+import logging
+import logging.config
+logging.config.fileConfig(path.join(PROJECT_ROOT, "logging.cfg"))
+
+# Import localsettings file, which may override settings defined here
+try:
+ from localsettings import *
+except ImportError:
+ pass
--- /dev/null
+{% extends "cas_base.html" %}
+
+{% block content %}
+ <form action='.' method='post'>
+ <fieldset>
+ <legend>Zaloguj się</legend>
+ {% if errors %}
+ <ul>
+ {% for error in errors %}
+ <li>{{ error|escape }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ <table style="border: none;">
+ {{ form.as_table }}
+ </table>
+ <p><input type="submit" value="Login"/></p>
+ </fieldset>
+ </form>
+{% endblock %}
+
\ No newline at end of file
--- /dev/null
+{% extends "cas_base.html" %}
+
+{% block title %}
+Logged out
+{% endblock %}
+
+{% block content %}
+ <h3>Logged out</h3>
+
+ <p>You have successfully logged out. To ensure that you are logged out of all services, please close your browser.</p>
+ {% if url %}<p><a href="{{ url }}">Click here</a> to return to {{ url }}</p>{% endif %}
+{% endblock %}
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <title>{% block title %}Fundacja Nowoczesna Polska - CAS{% block subtitle %}{% endblock subtitle %}{% endblock title%}</title>
+ {% block extrahead %}
+ {% endblock %}
+ </head>
+ <body id="{% block bodyid %}base{% endblock %}">
+ <div id="content">{% block content %} {% endblock %}</div>
+ </body>
+</html>
--- /dev/null
+# -*- coding: utf-8 -*-
+from django.conf.urls.defaults import *
+from django.contrib import admin
+from django.conf import settings
+
+admin.autodiscover()
+
+urlpatterns = patterns('',
+ # Admin panel
+ url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+ url(r'^admin/(.*)', admin.site.root),
+
+ # django-cas-provider
+ url(r'^', include('cas_provider.urls')),
+)
+
+
--- /dev/null
+def custom_attributes_callback(user):
+ return {
+ 'email': user.email,
+ 'firstname': user.first_name,
+ 'lastname': user.last_name,
+ }
--- /dev/null
+from __future__ import with_statement # needed for python 2.5
+from fabric.api import *
+from fabric.contrib import files
+
+import os
+
+
+# ==========
+# = Config =
+# ==========
+# Globals
+env.project_name = 'cas'
+env.use_south = False
+
+# Servers
+def staging():
+ """Use staging server"""
+ env.hosts = ['stigma.nowoczesnapolska.org.pl:2222']
+ env.user = 'platforma'
+ env.path = '/var/services/cas'
+ env.python = '/usr/bin/python'
+ env.virtualenv = '/usr/bin/virtualenv'
+ env.pip = '/usr/bin/pip'
+
+def production():
+ """Use production server"""
+ env.hosts = ['wolnelektury.pl:22123']
+ env.user = 'fundacja'
+ env.path = '/opt/lektury/cas'
+ env.python = '/opt/lektury/basevirtualenv/bin/python'
+ env.virtualenv = '/opt/lektury/basevirtualenv/bin/virtualenv'
+ env.pip = '/opt/lektury/basevirtualenv/bin/pip'
+
+
+# =========
+# = Tasks =
+# =========
+def test():
+ "Run the test suite and bail out if it fails"
+ require('hosts', 'path', provided_by=[staging, production])
+ result = run('cd %(path)s/%(project_name)s; %(python)s manage.py test' % env)
+
+def setup():
+ """
+ Setup a fresh virtualenv as well as a few useful directories, then run
+ a full deployment. virtualenv and pip should be already installed.
+ """
+ require('hosts', 'path', provided_by=[staging, production])
+
+ run('mkdir -p %(path)s; cd %(path)s; %(virtualenv)s --no-site-packages .;' % env, pty=True)
+ run('cd %(path)s; mkdir releases; mkdir shared; mkdir packages;' % env, pty=True)
+ run('cd %(path)s/releases; ln -s . current; ln -s . previous' % env, pty=True)
+ deploy()
+
+def deploy():
+ """
+ Deploy the latest version of the site to the servers,
+ install any required third party modules,
+ install the virtual host and then restart the webserver
+ """
+ require('hosts', 'path', provided_by=[staging, production])
+
+ import time
+ env.release = time.strftime('%Y-%m-%dT%H%M')
+
+ upload_tar_from_git()
+ upload_wsgi_script()
+ # upload_vhost_sample()
+ install_requirements()
+ copy_localsettings()
+ symlink_current_release()
+ migrate()
+ restart_webserver()
+
+def deploy_version(version):
+ "Specify a specific version to be made live"
+ require('hosts', 'path', provided_by=[localhost,webserver])
+ env.version = version
+ with cd(env.path):
+ run('rm releases/previous; mv releases/current releases/previous;', pty=True)
+ run('ln -s %(version)s releases/current' % env, pty=True)
+ restart_webserver()
+
+def rollback():
+ """
+ Limited rollback capability. Simple loads the previously current
+ version of the code. Rolling back again will swap between the two.
+ """
+ require('hosts', provided_by=[staging, production])
+ require('path')
+ with cd(env.path):
+ run('mv releases/current releases/_previous;', pty=True)
+ run('mv releases/previous releases/current;', pty=True)
+ run('mv releases/_previous releases/previous;', pty=True)
+ restart_webserver()
+
+
+# =====================================================================
+# = Helpers. These are called by other functions rather than directly =
+# =====================================================================
+def upload_tar_from_git():
+ "Create an archive from the current Git master branch and upload it"
+ print '>>> upload tar from git'
+ require('release', provided_by=[deploy])
+ local('git archive --format=tar master | gzip > %(release)s.tar.gz' % env)
+ run('mkdir -p %(path)s/releases/%(release)s' % env, pty=True)
+ run('mkdir -p %(path)s/packages' % env, pty=True)
+ put('%(release)s.tar.gz' % env, '%(path)s/packages/' % env)
+ run('cd %(path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
+ local('rm %(release)s.tar.gz' % env)
+
+def upload_vhost_sample():
+ "Create and upload Apache virtual host configuration sample"
+ print ">>> upload vhost sample"
+ files.upload_template('%(project_name)s.vhost.template' % env, '%(path)s/%(project_name)s.vhost.sample' % env, context=env)
+
+def upload_wsgi_script():
+ "Create and upload a wsgi script sample"
+ print ">>> upload wsgi script sample"
+ files.upload_template('%(project_name)s.wsgi.template' % env, '%(path)s/%(project_name)s.wsgi' % env, context=env)
+ run('chmod ug+x %(path)s/%(project_name)s.wsgi' % env)
+
+def install_requirements():
+ "Install the required packages from the requirements file using pip"
+ print '>>> install requirements'
+ require('release', provided_by=[deploy])
+ run('cd %(path)s; %(pip)s install -E . -r %(path)s/releases/%(release)s/requirements.txt' % env, pty=True)
+
+def copy_localsettings():
+ "Copy localsettings.py from root directory to release directory (if this file exists)"
+ print ">>> copy localsettings"
+ require('release', provided_by=[deploy])
+ require('path', provided_by=[staging, production])
+
+ with settings(warn_only=True):
+ run('cp %(path)s/localsettings.py %(path)s/releases/%(release)s/%(project_name)s' % env)
+
+def symlink_current_release():
+ "Symlink our current release"
+ print '>>> symlink current release'
+ require('release', provided_by=[deploy])
+ require('path', provided_by=[staging, production])
+ with cd(env.path):
+ run('rm releases/previous; mv releases/current releases/previous')
+ run('ln -s %(release)s releases/current' % env)
+
+def migrate():
+ "Update the database"
+ print '>>> migrate'
+ require('project_name', provided_by=[staging, production])
+ with cd('%(path)s/releases/current/%(project_name)s' % env):
+ run('../../../bin/python manage.py syncdb --noinput' % env, pty=True)
+ if env.use_south:
+ run('../../../bin/python manage.py migrate' % env, pty=True)
+
+def restart_webserver():
+ "Restart the web server"
+ print '>>> restart webserver'
+ run('touch %(path)s/releases/current/%(project_name)s/%(project_name)s.wsgi' % env)
--- /dev/null
+Chris Williams <chris@nitron.org>
+Marek Stepniowski <marek@stepniowski.com>
--- /dev/null
+Copyright (c) 2009, Chris Williams
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of the author nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null
+recursive-include cas_provider/templates *.html
+include README.rst
+include LICENSE
\ No newline at end of file
--- /dev/null
+===================
+django-cas-provider
+===================
+
+OVERVIEW
+=========
+
+django-cas-provider is a provider for the `Central Authentication Service <http://jasig.org/cas>`_. It supports CAS version 1.0 and parts of CAS version 2.0 protocol. It allows remote services to authenticate users for the purposes of Single Sign-On (SSO). For example, a user logs into a CAS server
+(provided by django-cas-provider) and can then access other services (such as email, calendar, etc) without re-entering her password for each service. For more details, see the `CAS wiki <http://www.ja-sig.org/wiki/display/CAS/Home>`_ and `Single Sign-On on Wikipedia <http://en.wikipedia.org/wiki/Single_Sign_On>`_.
+
+INSTALLATION
+=============
+
+To install, run the following command from this directory::
+
+ python setup.py install
+
+Or, put `cas_provider` somewhere on your Python path.
+
+USAGE
+======
+
+#. Add ``'cas_provider'`` to your ``INSTALLED_APPS`` tuple in *settings.py*.
+#. In *settings.py*, set ``LOGIN_URL`` to ``'/cas/login/'`` and ``LOGOUT_URL`` to ``'/cas/logout/'``
+#. In *urls.py*, put the following line: ``(r'^cas/', include('cas_provider.urls')),``
+#. Create login/logout templates (or modify the samples)
--- /dev/null
+from django.conf import settings
+
+__all__ = []
+
+_DEFAULTS = {
+ 'CAS_TICKET_EXPIRATION': 5, # In minutes
+ 'CAS_CUSTOM_ATTRIBUTES_CALLBACK': None,
+}
+
+for key, value in _DEFAULTS.iteritems():
+ try:
+ getattr(settings, key)
+ except AttributeError:
+ setattr(settings, key, value)
+ except ImportError:
+ pass
\ No newline at end of file
--- /dev/null
+from django.contrib import admin
+
+from cas_provider.models import ServiceTicket, LoginTicket
+
+class ServiceTicketAdmin(admin.ModelAdmin):
+ pass
+admin.site.register(ServiceTicket, ServiceTicketAdmin)
+
+class LoginTicketAdmin(admin.ModelAdmin):
+ pass
+admin.site.register(LoginTicket, LoginTicketAdmin)
\ No newline at end of file
--- /dev/null
+# Import etree from anywhere
+try:
+ # lxml http://codespeak.net/lxml/
+ from lxml import etree
+
+ # Define register_namespace function and ElementRoot for proper serialization
+ NSMAP = {}
+ def register_namespace(prefix, uri):
+ NSMAP[prefix] = uri
+
+ def ElementRoot(*args, **kwargs):
+ kwargs['nsmap'] = NSMAP
+ return etree.Element(*args, **kwargs)
+
+except ImportError:
+ try:
+ # normal cElementTree install
+ import cElementTree as etree
+ except ImportError:
+ # normal ElementTree install
+ import elementtree.ElementTree as etree
+
+ try:
+ register_namespace = etree.register_namespace
+ except AttributeError:
+ def register_namespace(prefix, uri):
+ etree._namespace_map[uri] = prefix
+
+ def ElementRoot(*args, **kwargs):
+ return etree.Element(*args, **kwargs)
+
+__all__ = ('etree', 'register_namespace', 'ElementRoot')
--- /dev/null
+from django import forms
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth import authenticate
+
+from cas_provider.utils import create_login_ticket
+
+class LoginForm(forms.Form):
+ username = forms.CharField(max_length=30)
+ password = forms.CharField(widget=forms.PasswordInput)
+ #warn = forms.BooleanField(required=False) # TODO: Implement
+ lt = forms.CharField(widget=forms.HiddenInput, initial=create_login_ticket)
+ def __init__(self, service=None, renew=None, gateway=None, request=None, *args, **kwargs):
+ super(LoginForm, self).__init__(*args, **kwargs)
+ self.request = request
+ if service is not None:
+ self.fields['service'] = forms.CharField(widget=forms.HiddenInput, initial=service)
\ No newline at end of file
--- /dev/null
+"""
+A management command which deletes expired service tickets (e.g.,
+from the database.
+
+Calls ``ServiceTickets.objects.delete_expired_users()``, which
+contains the actual logic for determining which accounts are deleted.
+
+"""
+
+from django.core.management.base import NoArgsCommand
+from django.core.management.base import CommandError
+from django.conf import settings
+
+import datetime
+
+from cas_provider.models import ServiceTicket, LoginTicket
+
+class Command(NoArgsCommand):
+ help = "Delete expired service tickets from the database"
+
+ def handle_noargs(self, **options):
+ 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
+ ticket.delete()
+ else:
+ print "%s not expired..." % ticket.ticket
+ tickets = LoginTicket.objects.all()
+ 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
+ ticket.delete()
+ else:
+ print "%s not expired..." % ticket.ticket
\ No newline at end of file
--- /dev/null
+from django.db import models
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.core.urlresolvers import get_callable
+
+from cas_provider.etree import etree, register_namespace, ElementRoot
+
+class ServiceTicket(models.Model):
+ user = models.ForeignKey(User)
+ service = models.URLField(verify_exists=False)
+ ticket = models.CharField(max_length=256)
+ created = models.DateTimeField(auto_now=True)
+
+ def __unicode__(self):
+ return "%s (%s) - %s" % (self.user.username, self.service, self.created)
+
+class LoginTicket(models.Model):
+ ticket = models.CharField(max_length=32)
+ created = models.DateTimeField(auto_now=True)
+
+ def __unicode__(self):
+ return "%s - %s" % (self.ticket, self.created)
+
+CAS_URI = 'http://www.yale.edu/tp/cas'
+register_namespace('cas', CAS_URI)
+CAS = '{%s}' % CAS_URI
+
+def auth_success_response(user):
+ attrs = {}
+ if settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK:
+ callback = get_callable(settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK)
+ attrs = callback(user)
+
+ response = ElementRoot(CAS + 'serviceResponse')
+ auth_success = etree.SubElement(response, CAS + 'authenticationSuccess')
+ username = etree.SubElement(auth_success, CAS + 'user')
+ username.text = user.username
+ for name, value in attrs.items():
+ element = etree.SubElement(auth_success, name)
+ element.text = value
+ return unicode(etree.tostring(response, encoding='utf-8'), 'utf-8')
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}
+Login
+{% endblock %}
+
+{% block content %}
+ <form action='.' method='post'>
+ <fieldset>
+ <legend>Log in to your account</legend>
+ {% if errors %}
+ <ul>
+ {% for error in errors %}
+ <li>{{ error|escape }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ <table style="border: none;">
+ {{ form.as_table }}
+ </table>
+ <p><input type="submit" value="Login"/></p>
+ </fieldset>
+ </form>
+{% endblock %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}
+Logged out
+{% endblock %}
+
+{% block content %}
+ <h3>Logged out</h3>
+
+ <p>You have successfully logged out. To ensure that you are logged out of all services, please close your browser.</p>
+ {% if url %}<p><a href="{{ url }}">Click here</a> to return to {{ url }}</p>{% endif %}
+{% endblock %}
--- /dev/null
+from django.conf.urls.defaults import *
+
+from cas_provider.views import *
+
+urlpatterns = patterns('',
+ url(r'^login/$', login),
+ url(r'^validate/$', validate),
+ url(r'^serviceValidate/$', service_validate),
+ url(r'^logout/$', logout),
+)
\ No newline at end of file
--- /dev/null
+from random import Random
+import string
+
+from cas_provider.models import ServiceTicket, LoginTicket
+
+def _generate_string(length=8, chars=string.ascii_letters + string.digits):
+ """ Generates a random string of the requested length. Used for creation of tickets. """
+ return ''.join(Random().sample(chars, length))
+
+def create_service_ticket(user, service):
+ """ Creates a new service ticket for the specified user and service.
+ Uses _generate_string.
+ """
+ ticket_string = 'ST-' + _generate_string(29) # Total ticket length = 29 + 3 = 32
+ ticket = ServiceTicket(service=service, user=user, ticket=ticket_string)
+ ticket.save()
+ return ticket
+
+def create_login_ticket():
+ """ Creates a new login ticket for the login form. Uses _generate_string. """
+ ticket_string = 'LT-' + _generate_string(29)
+ ticket = LoginTicket(ticket=ticket_string)
+ ticket.save()
+ return ticket_string
\ No newline at end of file
--- /dev/null
+from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.contrib.auth.models import User
+from django.contrib.auth import authenticate
+from django.contrib.auth import login as auth_login, logout as auth_logout
+
+from cas_provider.forms import LoginForm
+from cas_provider.models import ServiceTicket, LoginTicket, auth_success_response
+from cas_provider.utils import create_service_ticket
+
+import urlparse, urllib
+
+try:
+ from urlparse import parse_qs as url_parse_qs
+except ImportError:
+ from cgi import parse_qs as url_parse_qs
+
+
+import logging
+logger = logging.getLogger("fnp.cas.provider")
+
+__all__ = ['login', 'validate', 'service_validate', 'logout']
+
+def _add_query_param(url, param, value):
+ parsed = urlparse.urlparse(url)
+ query = url_parse_qs(parsed.query)
+ query[param] = [unicode(value, 'utf-8')]
+ query = [ ((k, v) if len(v) != 1 else (k, v[0])) for k, v in query.iteritems() ]
+ parsed = urlparse.ParseResult(parsed.scheme, parsed.netloc,
+ parsed.path, parsed.params,
+ urllib.urlencode(query), parsed.fragment)
+ return parsed.geturl()
+
+
+def login(request, template_name = 'cas/login.html', success_redirect = '/accounts/'):
+ service = request.GET.get('service', None)
+
+ if request.user.is_authenticated():
+ logger.info("User %s passed auth, service is %s", request.user, service)
+
+ if service is not None:
+ ticket = create_service_ticket(request.user, service)
+ target = _add_query_param(service, 'ticket', ticket.ticket)
+ logger.info("Redirecting to %s", target)
+ return HttpResponseRedirect(target)
+ else:
+ logger.info("Redirecting to default: %s", success_redirect)
+ return HttpResponseRedirect(success_redirect)
+
+ errors = []
+ if request.method == 'POST':
+ username = request.POST.get('username', None)
+ password = request.POST.get('password', None)
+ service = request.POST.get('service', None)
+ lt = request.POST.get('lt', None)
+
+ logger.debug("User %s logging in", username)
+ logger.info("Login submit: serivce = %s, Lticket=%s",service, lt)
+
+ try:
+ login_ticket = LoginTicket.objects.get(ticket = lt)
+ except:
+ errors.append('Login ticket expired. Please try again.')
+ else:
+ login_ticket.delete()
+ logger.debug("Auth")
+ user = authenticate(username = username, password = password)
+ if user is not None:
+ if user.is_active:
+ logger.debug("AuthLogin")
+ auth_login(request, user)
+ if service is not None:
+ ticket = create_service_ticket(user, service)
+ logger.info("Service=%s, ticket=%s", service, ticket)
+ target = _add_query_param(service, 'ticket', ticket.ticket)
+ logger.info("Redirecting to %s", target)
+ return HttpResponseRedirect(target)
+ else:
+ logger.info("Redirecting to default: %s", success_redirect)
+ return HttpResponseRedirect(success_redirect)
+ else:
+ errors.append('This account is disabled.')
+ else:
+ errors.append('Incorrect username and/or password.')
+
+ logger.debug("LOGIN GET, service = %s", service)
+ form = LoginForm(service)
+ return render_to_response(template_name, {'form': form, 'errors': errors}, context_instance = RequestContext(request))
+
+def validate(request):
+ service = request.GET.get('service', None)
+ ticket_string = request.GET.get('ticket', None)
+ if service is not None and ticket_string is not None:
+ try:
+ ticket = ServiceTicket.objects.get(ticket = ticket_string)
+ username = ticket.user.username
+ ticket.delete()
+ return HttpResponse("yes\n%s\n" % username)
+ except:
+ pass
+ return HttpResponse("no\n\n")
+
+def service_validate(request):
+ service = request.GET.get('service', None)
+ ticket_string = request.GET.get('ticket', None)
+ if service is None or ticket_string is None:
+ return HttpResponse('''<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+ <cas:authenticationFailure code="INVALID_REQUEST">
+ Not all required parameters were sent.
+ </cas:authenticationFailure>
+ </cas:serviceResponse>''', mimetype = 'text/xml')
+
+ try:
+ ticket = ServiceTicket.objects.get(ticket = ticket_string)
+ ticket.delete()
+ return HttpResponse(auth_success_response(ticket.user), mimetype = 'text/xml')
+ except ServiceTicket.DoesNotExist:
+ return HttpResponse('''<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+ <cas:authenticationFailure code="INVALID_TICKET">
+ The provided ticket is invalid.
+ </cas:authenticationFailure>
+ </cas:serviceResponse>''', mimetype = 'text/xml')
+
+def logout(request, template_name = 'cas/logout.html'):
+ url = request.GET.get('url', None)
+ auth_logout(request)
+ return render_to_response(template_name, {'url': url}, context_instance = RequestContext(request))
--- /dev/null
+[egg_info]
+tag_build = .dev
+tag_date = 1
\ No newline at end of file
--- /dev/null
+from setuptools import setup, find_packages
+
+setup(
+ name='django-cas-provider',
+ version='0.2',
+ description='A "provider" for the Central Authentication Service (http://jasig.org/cas)',
+ author='Chris Williams',
+ author_email='chris@nitron.org',
+ url='http://nitron.org/',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=['setuptools'],
+)
--- /dev/null
+--find-links=http://stigma.nowoczesnapolska.org.pl/pypi/
+
+Django>=1.1.1,<1.2
\ No newline at end of file