From: Alex Kamedov Date: Wed, 27 Apr 2011 15:40:37 +0000 (+0600) Subject: Merge branch 'vshulyak' into develop X-Git-Tag: 22.4~32^2~18^2~1 X-Git-Url: https://git.mdrn.pl/django-cas-provider.git/commitdiff_plain/087a24803f4e6749a5c32a88c62fcb922290635d?ds=sidebyside;hp=-c Merge branch 'vshulyak' into develop --- 087a24803f4e6749a5c32a88c62fcb922290635d diff --combined README.rst index 0ef1742,d197fbe..9e922c6 --- a/README.rst +++ b/README.rst @@@ -5,26 -5,18 +5,21 @@@ django-cas-provide OVERVIEW ========= - django-cas-provider is a provider for the `Central Authentication - Service `_. It supports CAS version 1.0. It allows - remote services to authenticate users for the purposes of - Single Sign-On (SSO). For example, a user logs into a CAS server - (provided by django-cas-provider) and can then access other services - (such as email, calendar, etc) without re-entering her password for - each service. For more details, see the `CAS wiki `_ - and `Single Sign-On on Wikipedia `_. + django-cas-provider is a provider for the `Central Authentication Service `_. It supports CAS version 1.0 and parts of CAS version 2.0 protocol. It allows remote services to authenticate users for the purposes of Single Sign-On (SSO). For example, a user logs into a CAS server + (provided by django-cas-provider) and can then access other services (such as email, calendar, etc) without re-entering her password for each service. For more details, see the `CAS wiki `_ and `Single Sign-On on Wikipedia `_. INSTALLATION ============= - To install, run the following command from this directory: + To install, run the following command from this directory:: - ``python setup.py install`` - python setup.py install ++ python setup.py install - Or, put cas_provider somewhere on your Python path. + Or, put `cas_provider` somewhere on your Python path. + +If you want use CAS v.2 protocol or above, you must install `lxml` package to correct work. + + USAGE ====== @@@ -32,132 -24,3 +27,132 @@@ #. In *settings.py*, set ``LOGIN_URL`` to ``'/cas/login/'`` and ``LOGOUT_URL`` to ``'/cas/logout/'`` #. In *urls.py*, put the following line: ``(r'^cas/', include('cas_provider.urls')),`` #. Create login/logout templates (or modify the samples) +#. Use 'cleanuptickets' management command to clean up expired tickets + +SETTINGS +========= + +CAS_TICKET_EXPIRATION - minutes to tickets expiration. Default is 5 minutes. + +CAS_CUSTOM_ATTRIBUTES_CALLBACK - name of callback to provide dictionary with +extended user attributes (may be used in CAS v.2 or above). Default is None. + +CAS_CUSTOM_ATTRIBUTES_FORMAT - name of custom attribute formatter callback will be +used to format custom user attributes. This package provide module `attribute_formatters` +with formatters for common used formats. Available formats styles are `RubyCAS`, `Jasig` +and `Name-Value. Default is Jasig style. See module source code for more details. + + +PROTOCOL DOCUMENTATION +===================== + +* `CAS Protocol ` +* `CAS 1 Architecture ` +* `CAS 2 Architecture ` +* `Proxy Authentication ` +* `CAS – Central Authentication Service ` +* `Proxy CAS Walkthrough ` + +PROVIDED VIEWS +============= + +login +--------- + +It has not required arguments. + +Optional arguments: + +* template_name - login form template name (default is 'cas/login.html') +* success_redirect - redirect after successful login if service GET argument is not provided + (default is settings.LOGIN_REDIRECT_URL) +* warn_template_name - warning page template name to allow login user to service if he + already authenticated in SSO (default is 'cas/warn.html') + +If request.GET has 'warn' argument and user has already authenticated in SSO it shows +warning message instead of generate Service Ticket and redirect. + +logout +----------- + +This destroys a client's single sign-on CAS session. The ticket-granting cookie is destroyed, +and subsequent requests to login view will not obtain service tickets until the user again +presents primary credentials (and thereby establishes a new single sign-on session). + +It has not required arguments. + +Optional arguments: + +* template_name - template name for page with successful logout message (default is 'cas/logout.html') + +validate +------------- + +It checks the validity of a service ticket. It is part of the CAS 1.0 protocol and thus does +not handle proxy authentication. + +It has not arguments. + +service_validate +------------------------- + +It checks the validity of a service ticket and returns an XML-fragment response via CAS 2.0 protocol. +Work with proxy is not supported yet. + +It has not arguments. + + +CUSTOM USER ATTRIBUTES FORMAT +=========================== + +Custom attribute format style may be changed in project settings with +CAS_CUSTOM_ATTRIBUTES_FORMAT constant. You can provide your own formatter callback +or specify existing in this package in `attribute_formatters` module. + +Attribute formatter callback takes two arguments: + +* `auth_success` - `cas:authenticationSuccess` node. It is `lxml.etree.SubElement`instance; +* `attrs` - dictionary with user attributes received from callback specified in + CAS_CUSTOM_ATTRIBUTES_CALLBACK in project settings. + - Example of generated XML below: ++Example of generated XML below:: + + + + jsmith + + + + PGTIOU-84678-8a9d2sfa23casd + + + + - * Name-Value style (provided in `cas_provider.attribute_formatters.name_value`): ++* Name-Value style (provided in `cas_provider.attribute_formatters.name_value`):: + + + + + + + + - * Jasig Style attributes (provided in `cas_provider.attribute_formatters.jasig`): ++* Jasig Style attributes (provided in `cas_provider.attribute_formatters.jasig`):: + + + Jasig + Smith + John + CN=Staff,OU=Groups,DC=example,DC=edu + CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu + + + - * RubyCAS style (provided in `cas_provider.attribute_formatters.ruby_cas`): ++* RubyCAS style (provided in `cas_provider.attribute_formatters.ruby_cas`):: + + RubyCAS + Smith + John + CN=Staff,OU=Groups,DC=example,DC=edu + CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu + diff --combined cas_provider/admin.py index b240d1f,5934487..f5058a4 --- a/cas_provider/admin.py +++ b/cas_provider/admin.py @@@ -1,13 -1,11 +1,14 @@@ from django.contrib import admin +from models import * -from cas_provider.models import ServiceTicket, LoginTicket + class ServiceTicketAdmin(admin.ModelAdmin): - pass -admin.site.register(ServiceTicket, ServiceTicketAdmin) + list_display = ('user', 'service', 'created') + list_filter = ('created',) class LoginTicketAdmin(admin.ModelAdmin): - pass -admin.site.register(LoginTicket, LoginTicketAdmin) + list_display = ('ticket', 'created') + list_filter = ('created',) + +admin.site.register(ServiceTicket, ServiceTicketAdmin) +admin.site.register(LoginTicket, LoginTicketAdmin) diff --combined cas_provider/views.py index 4a7e9b8,c2b3d58..d738dcd --- a/cas_provider/views.py +++ b/cas_provider/views.py @@@ -1,76 -1,62 +1,76 @@@ -from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect +from django.conf import settings +from django.contrib.auth import login as auth_login, logout as auth_logout +from django.core.urlresolvers import get_callable +from django.http import HttpResponse, 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 forms import LoginForm +from models import ServiceTicket, LoginTicket + + +__all__ = ['login', 'validate', 'logout', 'service_validate'] + -from cas_provider.forms import LoginForm -from cas_provider.models import ServiceTicket, LoginTicket, auth_success_response -from cas_provider.utils import create_service_ticket +INVALID_TICKET = 'INVALID_TICKET' +INVALID_SERVICE = 'INVALID_SERVICE' +INVALID_REQUEST = 'INVALID_REQUEST' +INTERNAL_ERROR = 'INTERNAL_ERROR' -__all__ = ['login', 'validate', 'service_validate', 'logout'] +ERROR_MESSAGES = ( + (INVALID_TICKET, u'The provided ticket is invalid.'), + (INVALID_SERVICE, u'Service is invalid'), + (INVALID_REQUEST, u'Not all required parameters were sent.'), + (INTERNAL_ERROR, u'An internal error occurred during ticket validation'), +) -def login(request, template_name='cas/login.html', success_redirect='/accounts/'): + +def login(request, template_name='cas/login.html', \ + success_redirect=settings.LOGIN_REDIRECT_URL, + warn_template_name='cas/warn.html'): service = request.GET.get('service', None) if request.user.is_authenticated(): if service is not None: - ticket = create_service_ticket(request.user, service) - if service.find('?') == -1: - return HttpResponseRedirect(service + '?ticket=' + ticket.ticket) - else: - return HttpResponseRedirect(service + '&ticket=' + ticket.ticket) + if request.GET.get('warn', False): + return render_to_response(warn_template_name, { + 'service': service, + 'warn': False + }, context_instance=RequestContext(request)) + ticket = ServiceTicket.objects.create(service=service, user=request.user) + return HttpResponseRedirect(ticket.get_redirect_url()) else: 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) - - try: - login_ticket = LoginTicket.objects.get(ticket=lt) - except: - errors.append('Login ticket expired. Please try again.') - else: - login_ticket.delete() - user = authenticate(username=username, password=password) - if user is not None: - if user.is_active: - auth_login(request, user) - if service is not None: - ticket = create_service_ticket(user, service) - return HttpResponseRedirect(service + '?ticket=' + ticket.ticket) - else: - return HttpResponseRedirect(success_redirect) - else: - errors.append('This account is disabled.') - else: - errors.append('Incorrect username and/or password.') - form = LoginForm(service) - return render_to_response(template_name, {'form': form, 'errors': errors}, context_instance=RequestContext(request)) - + form = LoginForm(request.POST) + if form.is_valid(): + user = form.get_user() + auth_login(request, user) + service = form.cleaned_data.get('service') + if service is not None: + ticket = ServiceTicket.objects.create(service=service, user=user) + success_redirect = ticket.get_redirect_url() + return HttpResponseRedirect(success_redirect) + else: + form = LoginForm(initial={ + 'service': service, + 'lt': LoginTicket.objects.create() + }) + return render_to_response(template_name, { + 'form': form, + 'errors': form.get_errors() + }, context_instance=RequestContext(request)) + + def validate(request): + """Validate ticket via CAS v.1 protocol""" service = request.GET.get('service', None) ticket_string = request.GET.get('ticket', None) if service is not None and ticket_string is not None: + #renew = request.GET.get('renew', True) + #if not renew: + # TODO: check user SSO session try: ticket = ServiceTicket.objects.get(ticket=ticket_string) + assert ticket.service == service username = ticket.user.username ticket.delete() return HttpResponse("yes\n%s\n" % username) @@@ -78,62 -64,31 +78,65 @@@ pass return HttpResponse("no\n\n") + - def logout(request, template_name='cas/logout.html'): ++def logout(request, template_name='cas/logout.html', auto_redirect=False): + url = request.GET.get('url', None) - auth_logout(request) ++ if request.user.is_authenticated(): ++ auth_logout(request) ++ if url and auto_redirect: ++ return HttpResponseRedirect(url) + return render_to_response(template_name, {'url': url}, \ + context_instance=RequestContext(request)) + + def service_validate(request): + """Validate ticket via CAS v.2 protocol""" service = request.GET.get('service', None) ticket_string = request.GET.get('ticket', None) if service is None or ticket_string is None: - return HttpResponse(''' - - Not all required parameters were sent. - - ''', mimetype='text/xml') - + return _cas2_error_response(INVALID_REQUEST) + 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(''' - - The provided ticket is invalid. + return _cas2_error_response(INVALID_TICKET) + + if ticket.service != service: + ticket.delete() + return _cas2_error_response(INVALID_SERVICE) + + user = ticket.user + ticket.delete() + return _cas2_sucess_response(user) + + +def _cas2_error_response(code): + return HttpResponse(u'''' + + %(message)s - ''', mimetype='text/xml') + ''' % { + 'code': code, + 'message': dict(ERROR_MESSAGES).get(code) + }, mimetype='text/xml') -def logout(request, template_name='cas/logout.html', auto_redirect=False): - url = request.GET.get('url', None) - if request.user.is_authenticated(): - auth_logout(request) - if url and auto_redirect: - return HttpResponseRedirect(url) - return render_to_response(template_name, {'url': url}, context_instance=RequestContext(request)) + +def _cas2_sucess_response(user): + return HttpResponse(auth_success_response(user), mimetype='text/xml') + + +def auth_success_response(user): + from attribute_formatters import CAS, NSMAP, etree + + response = etree.Element(CAS + 'serviceResponse', nsmap=NSMAP) + auth_success = etree.SubElement(response, CAS + 'authenticationSuccess') + username = etree.SubElement(auth_success, CAS + 'user') + username.text = user.username + + if settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK: + callback = get_callable(settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK) + attrs = callback(user) + if len(attrs) > 0: + formater = get_callable(settings.CAS_CUSTOM_ATTRIBUTES_FORMATER) + formater(auth_success, attrs) + return unicode(etree.tostring(response, encoding='utf-8'), 'utf-8') diff --combined setup.py index 2d65aaf,a9b74ae..06e5d4a --- a/setup.py +++ b/setup.py @@@ -1,14 -1,14 +1,15 @@@ from setuptools import setup, find_packages - + + setup( name='django-cas-provider', - version='0.2.1', + version='0.2.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'], )