Merge branch 'develop'
authorAlex Kamedov <alex@kamedov.ru>
Sun, 24 Apr 2011 15:26:33 +0000 (21:26 +0600)
committerAlex Kamedov <alex@kamedov.ru>
Sun, 24 Apr 2011 15:26:33 +0000 (21:26 +0600)
12 files changed:
README.rst
cas_provider/__init__.py
cas_provider/fixtures/cas_users.json [new file with mode: 0644]
cas_provider/forms.py
cas_provider/locale/ru/LC_MESSAGES/django.mo
cas_provider/locale/ru/LC_MESSAGES/django.po
cas_provider/models.py
cas_provider/templates/cas/warn.html [new file with mode: 0644]
cas_provider/tests.py [new file with mode: 0644]
cas_provider/urls.py
cas_provider/utils.py [deleted file]
cas_provider/views.py

index d313340..bc2874d 100644 (file)
@@ -40,4 +40,64 @@ SETTINGS
 =========
 
 CAS_TICKET_EXPIRATION - minutes to tickets expiration (default is 5 minutes)
+CAS_CHECK_SERVICE - check if ticket service is equal with service GET argument
+
+PROTOCOL DOCUMENTATION
+=====================
+
+* `CAS Protocol <http://www.jasig.org/cas/protocol>`
+* `CAS 1 Architecture <http://www.jasig.org/cas/cas1-architecture>`
+* `CAS 2 Architecture <http://www.jasig.org/cas/cas2-architecture>`
+* `Proxy Authentication <http://www.jasig.org/cas/proxy-authentication>`
+* `CAS – Central Authentication Service <http://www.jusfortechies.com/cas/overview.html>`
+* `Proxy CAS Walkthrough <https://wiki.jasig.org/display/CAS/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 - it shows warning message if user has already
+authenticated in SSO 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.
+
 
index 1a719b4..91b3b2c 100644 (file)
@@ -4,6 +4,7 @@ __all__ = []
 
 _DEFAULTS = {
     'CAS_TICKET_EXPIRATION': 5, # In minutes
+    'CAS_CHECK_SERVICE': False,
 }
 
 for key, value in _DEFAULTS.iteritems():
@@ -12,4 +13,4 @@ for key, value in _DEFAULTS.iteritems():
     except AttributeError:
         setattr(settings, key, value)
     except ImportError:
-        pass
\ No newline at end of file
+        pass
diff --git a/cas_provider/fixtures/cas_users.json b/cas_provider/fixtures/cas_users.json
new file mode 100644 (file)
index 0000000..f31d5cb
--- /dev/null
@@ -0,0 +1,130 @@
+[
+  {
+    "pk": 1, 
+    "model": "auth.group", 
+    "fields": {
+      "name": "editor", 
+      "permissions": []
+    }
+  }, 
+  {
+    "pk": 2, 
+    "model": "auth.group", 
+    "fields": {
+      "name": "author", 
+      "permissions": []
+    }
+  }, 
+  {
+    "pk": 1, 
+    "model": "auth.user", 
+    "fields": {
+      "username": "root", 
+      "first_name": "", 
+      "last_name": "", 
+      "is_active": true, 
+      "is_superuser": true, 
+      "is_staff": true, 
+      "last_login": "2011-04-24 11:29:11", 
+      "groups": [], 
+      "user_permissions": [], 
+      "password": "sha1$602c5$ba8608296f6bfcb352e978084b337a90d586ecc3", 
+      "email": "root@example.com", 
+      "date_joined": "2010-07-04 13:33:14"
+    }
+  }, 
+  {
+    "pk": 26, 
+    "model": "auth.user", 
+    "fields": {
+      "username": "active", 
+      "first_name": "", 
+      "last_name": "", 
+      "is_active": true, 
+      "is_superuser": false, 
+      "is_staff": false, 
+      "last_login": "2011-04-01 12:42:53", 
+      "groups": [], 
+      "user_permissions": [], 
+      "password": "sha1$7dfb4$d19f8340a01b597089dfde6dc17bc5288c1f863e", 
+      "email": "active@example.com", 
+      "date_joined": "2011-04-01 11:12:45"
+    }
+  }, 
+  {
+    "pk": 30, 
+    "model": "auth.user", 
+    "fields": {
+      "username": "author", 
+      "first_name": "", 
+      "last_name": "", 
+      "is_active": true, 
+      "is_superuser": false, 
+      "is_staff": true, 
+      "last_login": "2011-04-24 11:32:16", 
+      "groups": [
+        2
+      ], 
+      "user_permissions": [], 
+      "password": "sha1$6c580$01509bea19e3ade9f1bcf303205a7cb10ce6762d", 
+      "email": "", 
+      "date_joined": "2011-04-24 11:32:16"
+    }
+  }, 
+  {
+    "pk": 29, 
+    "model": "auth.user", 
+    "fields": {
+      "username": "editor", 
+      "first_name": "", 
+      "last_name": "", 
+      "is_active": true, 
+      "is_superuser": false, 
+      "is_staff": true, 
+      "last_login": "2011-04-24 11:31:50", 
+      "groups": [
+        1
+      ], 
+      "user_permissions": [], 
+      "password": "sha1$3be01$b6aa05c61fc52edae3055c55e160d4cfd4756d91", 
+      "email": "editor@exapmle.com", 
+      "date_joined": "2011-04-24 11:31:50"
+    }
+  }, 
+  {
+    "pk": 27, 
+    "model": "auth.user", 
+    "fields": {
+      "username": "nonactive", 
+      "first_name": "", 
+      "last_name": "", 
+      "is_active": false, 
+      "is_superuser": false, 
+      "is_staff": false, 
+      "last_login": "2011-04-24 11:31:00", 
+      "groups": [], 
+      "user_permissions": [], 
+      "password": "sha1$0bf10$d60f146d15e4fe3cb0de5a607a17902d0f63a95c", 
+      "email": "", 
+      "date_joined": "2011-04-24 11:31:00"
+    }
+  }, 
+  {
+    "pk": 28, 
+    "model": "auth.user", 
+    "fields": {
+      "username": "staff", 
+      "first_name": "", 
+      "last_name": "", 
+      "is_active": true, 
+      "is_superuser": false, 
+      "is_staff": true, 
+      "last_login": "2011-04-24 11:31:26", 
+      "groups": [], 
+      "user_permissions": [], 
+      "password": "sha1$5df85$bb4c1894a866fb86465d28831000af20316233d5", 
+      "email": "staff@example.com", 
+      "date_joined": "2011-04-24 11:31:26"
+    }
+  }
+]
\ No newline at end of file
index 80b8913..47c2fdc 100644 (file)
@@ -1,15 +1,52 @@
 from django import forms
+from django.conf import settings
+from django.contrib.auth import authenticate
+from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext_lazy as _
+from models import LoginTicket
+import datetime
+
+
+__all__ = ['LoginForm', ]
 
-from utils import create_login_ticket
 
 class LoginForm(forms.Form):
     username = forms.CharField(max_length=30, label=_('username'))
     password = forms.CharField(widget=forms.PasswordInput, label=_('password'))
-    #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):
+    lt = forms.CharField(widget=forms.HiddenInput)
+    service = forms.CharField(widget=forms.HiddenInput, required=False)
+
+    def __init__(self, *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
+        self._user = None
+
+    def clean_lt(self):
+        ticket = self.cleaned_data['lt']
+        timeframe = datetime.datetime.now() - \
+                    datetime.timedelta(minutes=settings.CAS_TICKET_EXPIRATION)
+        try:
+            return LoginTicket.objects.get(ticket=ticket, created__gte=timeframe)
+        except LoginTicket.DoesNotExist:
+            raise ValidationError(_('Login ticket expired. Please try again.'))
+        return ticket
+
+    def clean(self):
+        username = self.cleaned_data.get('username')
+        password = self.cleaned_data.get('password')
+        user = authenticate(username=username, password=password)
+        if user is None:
+            raise ValidationError(_('Incorrect username and/or password.'))
+        if not user.is_active:
+            raise ValidationError(_('This account is disabled.'))
+        self._user = user
+        self.cleaned_data.get('lt').delete()
+        return self.cleaned_data
+
+    def get_user(self):
+        return self._user
+    
+    def get_errors(self):
+        errors = []
+        for k, error in self.errors.items():
+            errors += [e for e in error]
+        return errors
index b0e243a..c02ed63 100644 (file)
Binary files a/cas_provider/locale/ru/LC_MESSAGES/django.mo and b/cas_provider/locale/ru/LC_MESSAGES/django.mo differ
index 8f3cbec..42a8506 100644 (file)
@@ -1,12 +1,12 @@
 # SOME DESCRIPTIVE TITLE.
 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
-# Volf <alex@kamedov.ru>, 2011.
+# Alex Kamedov <alex@kamedov.ru>, 2011.
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2011-04-07 11:57+0600\n"
+"POT-Creation-Date: 2011-04-24 18:51+0600\n"
 "PO-Revision-Date: 2011-04-07 12:01+0600\n"
 "Last-Translator: Volf <alex@kamedov.ru>\n"
 "Language-Team: delux\n"
@@ -14,58 +14,58 @@ msgstr ""
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%"
-"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
 "X-Generator: Virtaal 0.6.1\n"
 
-#: forms.py:9
+#: forms.py:14
 msgid "username"
 msgstr "имя пользователя"
 
-#: forms.py:10
+#: forms.py:15
 msgid "password"
 msgstr "пароль"
 
-#: models.py:6
-msgid "user"
-msgstr "полÑ\8cзоваÑ\82елÑ\8c"
+#: forms.py:30
+msgid "Login ticket expired. Please try again."
+msgstr "Ð\98Ñ\81Ñ\82ек Ñ\81Ñ\80ок Ð´ÐµÐ¹Ñ\81Ñ\82виÑ\8f Ð±Ð¸Ð»ÐµÑ\82а Ð²Ñ\85ода. Ð\9fожалÑ\83йÑ\81Ñ\82а, Ð¿Ð¾Ð¿Ñ\80обÑ\83йÑ\82е ÐµÑ\89е Ñ\80аз."
 
-#: models.py:7
-msgid "service"
-msgstr "сервис"
+#: forms.py:38
+msgid "Incorrect username and/or password."
+msgstr "Неверное имя пользователя и/или пароль."
+
+#: forms.py:40
+msgid "This account is disabled."
+msgstr "Эта учетная запись отключена."
 
-#: models.py:8 models.py:19
+#: models.py:14
 msgid "ticket"
 msgstr "билет"
 
-#: models.py:9 models.py:20
+#: models.py:15
 msgid "created"
 msgstr "создан"
 
-#: models.py:12
+#: models.py:34
+msgid "user"
+msgstr "пользователь"
+
+#: models.py:35
+msgid "service"
+msgstr "сервис"
+
+#: models.py:40
 msgid "Service Ticket"
 msgstr "Билет для сервиса"
 
-#: models.py:13
+#: models.py:41
 msgid "Service Tickets"
 msgstr "Билеты для сервисов"
 
-#: models.py:23
+#: models.py:59
 msgid "Login Ticket"
 msgstr "Билет для входа"
 
-#: models.py:24
+#: models.py:60
 msgid "Login Tickets"
 msgstr "Билеты для входа"
-
-#: views.py:36
-msgid "Login ticket expired. Please try again."
-msgstr "Истек срок действия билета входа. Пожалуйста, попробуйте еще раз."
-
-#: views.py:49
-msgid "This account is disabled."
-msgstr "Эта учетная запись отключена."
-
-#: views.py:51
-msgid "Incorrect username and/or password."
-msgstr "Неверное имя пользователя и/или пароль."
index 6eecc3e..ec4b695 100644 (file)
@@ -1,29 +1,60 @@
-from django.db import models
 from django.contrib.auth.models import User
+from django.db import models
 from django.utils.translation import ugettext_lazy as _
+from random import Random
+import string
+import urllib
+import urlparse
+
 
 __all__ = ['ServiceTicket', 'LoginTicket']
 
-class ServiceTicket(models.Model):
+
+class BaseTicket(models.Model):
+    ticket = models.CharField(_('ticket'), max_length=32)
+    created = models.DateTimeField(_('created'), auto_now=True)
+
+    class Meta:
+        abstract = True
+
+    def __init__(self, *args, **kwargs):
+        if 'ticket' not in kwargs:
+            kwargs['ticket'] = self._generate_ticket()
+        super(BaseTicket, self).__init__(*args, **kwargs)
+
+    def __unicode__(self):
+        return self.ticket
+
+    def _generate_ticket(self, length=29, 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)))
+
+
+class ServiceTicket(BaseTicket):
     user = models.ForeignKey(User, verbose_name=_('user'))
     service = models.URLField(_('service'), verify_exists=False)
-    ticket = models.CharField(_('ticket'), max_length=256)
-    created = models.DateTimeField(_('created'), auto_now=True)
+
+    prefix = 'ST'
 
     class Meta:
         verbose_name = _('Service Ticket')
         verbose_name_plural = _('Service Tickets')
 
-    def __unicode__(self):
-        return "%s (%s) - %s" % (self.user.username, self.service, self.created)
+    def get_redirect_url(self):
+        parsed = urlparse.urlparse(self.service)
+        query = urlparse.parse_qs(parsed.query)
+        query['ticket'] = [self.ticket]
+        query = [ ((k, v) if len(v) > 1 else (k, v[0])) for k, v in query.iteritems()]
+        parsed = urlparse.ParseResult(parsed.scheme, parsed.netloc,
+                                  parsed.path, parsed.params,
+                                  urllib.urlencode(query), parsed.fragment)
+        return parsed.geturl()
 
-class LoginTicket(models.Model):
-    ticket = models.CharField(_('ticket'), max_length=32)
-    created = models.DateTimeField(_('created'), auto_now=True)
+
+class LoginTicket(BaseTicket):
+
+    prefix = 'LT'
 
     class Meta:
         verbose_name = _('Login Ticket')
         verbose_name_plural = _('Login Tickets')
-
-    def __unicode__(self):
-        return "%s - %s" % (self.ticket, self.created)
diff --git a/cas_provider/templates/cas/warn.html b/cas_provider/templates/cas/warn.html
new file mode 100644 (file)
index 0000000..58f4ed8
--- /dev/null
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block title %}
+Warning
+{% endblock %}
+
+{% block content %}
+  <form action='.' method='get'>
+    <fieldset>
+      <legend>Confirm to log in to {{ service }}</legend>
+         <input type="hidden" name="service" value="{{ service }}">
+      <p><input type="submit" value="Login"/></p>
+    </fieldset>
+  </form>
+{% endblock %}
diff --git a/cas_provider/tests.py b/cas_provider/tests.py
new file mode 100644 (file)
index 0000000..c876148
--- /dev/null
@@ -0,0 +1,143 @@
+from cas_provider.models import ServiceTicket
+from cas_provider.views import _cas2_sucess_response, _cas2_error_response, \
+    INVALID_TICKET
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+from urlparse import urlparse
+
+
+class ViewsTest(TestCase):
+
+    fixtures = ['cas_users.json', ]
+
+    def setUp(self):
+        self.service = 'http://example.com/'
+
+
+    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 test_logout(self):
+        response = self._login_user('root', '123')
+        self._validate_cas1(response, True)
+
+        response = self.client.get(reverse('cas_logout'), follow=False)
+        self.assertEqual(response.status_code, 200)
+
+        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')
+        self._validate_cas2(response, True)
+
+    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 _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):
+        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_service_validate'), {'ticket': ticket, 'service': self.service}, follow=False)
+            self.assertEqual(response.status_code, 200)
+            self.assertEqual(response.content, _cas2_sucess_response(self.username).content)
+        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)
+
+
+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__)
+
index 8edc91a..16a6744 100644 (file)
@@ -1,9 +1,9 @@
-from django.conf.urls.defaults import *
+from django.conf.urls.defaults import patterns, url
 
-from views import *
 
-urlpatterns = patterns('',
-    url(r'^login/', login),
-    url(r'^validate/', validate),
-    url(r'^logout/', logout),
-)
\ No newline at end of file
+urlpatterns = patterns('cas_provider.views',
+    url(r'^login/?$', 'login', name='cas_login'),
+    url(r'^validate/?$', 'validate', name='cas_validate'),
+    url(r'^serviceValidate/?$', 'service_validate', name='cas_service_validate'),
+    url(r'^logout/?$', 'logout', name='cas_logout'),
+)
diff --git a/cas_provider/utils.py b/cas_provider/utils.py
deleted file mode 100644 (file)
index 04a5c12..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-from random import Random
-import string
-
-from models import ServiceTicket, LoginTicket
-
-def _generate_string(length=8, chars=string.ascii_letters + string.digits):
-    """ Generates a random string of the requested length. Used for creation of tickets. """
-    return ''.join(Random().sample(chars, length))
-
-def create_service_ticket(user, service):
-    """ Creates a new service ticket for the specified user and service.
-        Uses _generate_string.
-    """
-    ticket_string = 'ST-' + _generate_string(29) # Total ticket length = 29 + 3 = 32
-    ticket = ServiceTicket(service=service, user=user, ticket=ticket_string)
-    ticket.save()
-    return ticket
-
-def create_login_ticket():
-    """ Creates a new login ticket for the login form. Uses _generate_string. """
-    ticket_string = 'LT-' + _generate_string(29)
-    ticket = LoginTicket(ticket=ticket_string)
-    ticket.save()
-    return ticket_string
\ No newline at end of file
index 5343232..32ed6e9 100644 (file)
+from django.conf import settings
+from django.contrib.auth import login as auth_login, \
+    logout as auth_logout
 from django.http import HttpResponse, HttpResponseRedirect
 from django.shortcuts import render_to_response
 from django.template import RequestContext
-from django.contrib.auth import authenticate
-from django.contrib.auth import login as auth_login, logout as auth_logout
-from django.utils.translation import ugettext_lazy as _
-
 from forms import LoginForm
 from models import ServiceTicket, LoginTicket
-from utils import create_service_ticket
 
-__all__ = ['login', 'validate', 'logout']
 
-def login(request, template_name='cas/login.html', success_redirect='/accounts/'):
+__all__ = ['login', 'validate', 'logout', 'service_validate']
+
+
+INVALID_TICKET = 'INVALID_TICKET'
+INVALID_SERVICE = 'INVALID_SERVICE'
+INVALID_REQUEST = 'INVALID_REQUEST'
+INTERNAL_ERROR = 'INTERNAL_ERROR'
+
+ERROR_MESSAGES = (
+    (INVALID_TICKET, u'The provided ticket is invalid.'),
+    (INVALID_SERVICE, u'Service is invalid'),
+    (INVALID_REQUEST, u'Not all required parameters were sent.'),
+    (INTERNAL_ERROR, u'An internal error occurred during ticket validation'),
+)
+
+
+def 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)
             username = ticket.user.username
             ticket.delete()
-            return HttpResponse("yes\r\n%s\r\n" % username)
+            return HttpResponse("yes\n%s\n" % username)
         except:
             pass
-    return HttpResponse("no\r\n\r\n")
-    
+    return HttpResponse("no\n\n")
+
+
 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))
+    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 _cas2_error_response(INVALID_REQUEST)
+
+    try:
+        ticket = ServiceTicket.objects.get(ticket=ticket_string)
+    except ServiceTicket.DoesNotExist:
+        return _cas2_error_response(INVALID_TICKET)
+
+    if settings.CAS_CHECK_SERVICE and ticket.service != service:
+        ticket.delete()
+        return _cas2_error_response(INVALID_SERVICE)
+
+    username = ticket.user.username
+    ticket.delete()
+    return _cas2_sucess_response(username)
+
+
+def _cas2_error_response(code):
+    return HttpResponse(u''''<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+            <cas:authenticationFailure code="%(code)s">
+                %(message)s
+            </cas:authenticationFailure>
+        </cas:serviceResponse>''' % {
+            'code': code,
+            'message': dict(ERROR_MESSAGES).get(code)
+    }, mimetype='text/xml')
+
+
+def _cas2_sucess_response(username):
+    return HttpResponse(u'''<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+        <cas:authenticationSuccess>
+            <cas:user>%(username)s</cas:user>
+        </cas:authenticationSuccess>
+    </cas:serviceResponse>''' % {'username': username}, mimetype='text/xml')