Commit 63f5b2ca authored by Valentin Samir's avatar Valentin Samir

Merge branch 'master' into federate

parents a4d70d30 a1edd2f3
[run]
branch = True
source = cas_server
omit =
cas_server/migrations*
cas_server/management/*
cas_server/tests/*
[report]
exclude_lines =
pragma: no cover
......
......@@ -2,19 +2,20 @@ language: python
python:
- "2.7"
env:
global:
- PIP_DOWNLOAD_CACHE=$HOME/.pip_cache
matrix:
- TOX_ENV=coverage
- TOX_ENV=flake8
- TOX_ENV=check_rst
- TOX_ENV=py27-django17
- TOX_ENV=py27-django18
- TOX_ENV=py27-django19
- TOX_ENV=py34-django17
- TOX_ENV=py34-django18
- TOX_ENV=py34-django19
- TOX_ENV=flake8
cache:
directories:
- $HOME/.pip-cache/
- $HOME/.cache/pip/
- $HOME/build/nitmir/django-cas-server/.tox/
install:
- "travis_retry pip install setuptools --upgrade"
- "pip install tox"
......@@ -22,4 +23,3 @@ script:
- tox -e $TOX_ENV
after_script:
- cat .tox/$TOX_ENV/log/*.log
.PHONY: clean build install dist test_venv test_project
.PHONY: build dist
VERSION=`python setup.py -V`
build:
python setup.py build
install:
python setup.py install
install: dist
pip -V
pip install --no-deps --upgrade --force-reinstall --find-links ./dist/django-cas-server-${VERSION}.tar.gz django-cas-server
uninstall:
pip uninstall django-cas-server || true
clean_pyc:
find ./ -name '*.pyc' -delete
......@@ -16,18 +20,23 @@ clean_tox:
rm -rf .tox
clean_test_venv:
rm -rf test_venv
clean: clean_pyc clean_build
clean_all: clean_pyc clean_build clean_tox clean_test_venv
clean_coverage:
rm -rf coverage.xml .coverage htmlcov
clean_tild_backup:
find ./ -name '*~' -delete
clean: clean_pyc clean_build clean_coverage clean_tild_backup
clean_all: clean clean_tox clean_test_venv
dist:
python setup.py sdist
test_venv:
mkdir -p test_venv
test_venv/bin/python:
virtualenv test_venv
test_venv/bin/pip install -U --requirement requirements.txt
test_venv/bin/pip install -U --requirement requirements-dev.txt Django
test_venv/cas/manage.py:
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
......@@ -38,20 +47,16 @@ test_venv/cas/manage.py:
test_venv/bin/python test_venv/cas/manage.py migrate
test_venv/bin/python test_venv/cas/manage.py createsuperuser
test_project: test_venv test_venv/cas/manage.py
test_venv: test_venv/bin/python
test_project: test_venv/cas/manage.py
@echo "##############################################################"
@echo "A test django project was created in $(realpath test_venv/cas)"
run_test_server: test_project
run_server: test_project
test_venv/bin/python test_venv/cas/manage.py runserver
coverage: test_venv
test_venv/bin/pip install coverage
test_venv/bin/coverage run --source='cas_server' --omit='cas_server/migrations*' run_tests
test_venv/bin/coverage html
run_tests: test_venv
python setup.py check --restructuredtext --stric
test_venv/bin/py.test --cov=cas_server --cov-report html
rm htmlcov/coverage_html.js # I am really pissed off by those keybord shortcuts
coverage_codacy: coverage
test_venv/bin/coverage xml
test_venv/bin/pip install codacy-coverage
test_venv/bin/python-codacy-coverage -r coverage.xml
......@@ -22,11 +22,11 @@ CAS Server is a Django application implementing the `CAS Protocol 3.0 Specificat
By defaut, the authentication process use django internal users but you can easily
use any sources (see auth classes in the auth.py file)
The defaut login/logout template use `django-bootstrap3 <https://github.com/dyve/django-bootstrap3>`_
The defaut login/logout template use `django-bootstrap3 <https://github.com/dyve/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 <https://github.com/dyve/django-bootstrap3>`_ < 7.0.0
`django-bootstrap3 <https://github.com/dyve/django-bootstrap3>`__ < 7.0.0
like the 6.2.2 version.
Features
......@@ -43,7 +43,7 @@ Features
Quick start
-----------
0. If you want to make a virtualenv for ``django-cas-server``, you will need the following
1. If you want to make a virtualenv for ``django-cas-server``, you will need the following
dependencies on a bare debian like system::
virtualenv build-essential python-dev libxml2-dev libxslt1-dev zlib1g-dev
......@@ -53,7 +53,7 @@ Quick start
If you intend to run the tox tests you will also need ``python3.4-dev`` depending of the current
version of python3 on your system.
1. Add "cas_server" to your INSTALLED_APPS setting like this::
2. Add "cas_server" to your INSTALLED_APPS setting like this::
INSTALLED_APPS = (
'django.contrib.admin',
......@@ -71,7 +71,7 @@ Quick start
...
)
2. Include the cas_server URLconf in your project urls.py like this::
3. Include the cas_server URLconf in your project urls.py like this::
urlpatterns = [
url(r'^admin/', admin.site.urls),
......@@ -79,22 +79,22 @@ Quick start
url(r'^cas/', include('cas_server.urls', namespace="cas_server")),
]
3. Run `python manage.py migrate` to create the cas_server models.
4. Run `python manage.py migrate` to create the cas_server models.
4. You should add some management commands to a crontab: ``clearsessions``,
5. You should add some management commands to a crontab: ``clearsessions``,
``cas_clean_tickets`` and ``cas_clean_sessions``.
* ``clearsessions``: please see `Clearing the session store <https://docs.djangoproject.com/en/stable/topics/http/sessions/#clearing-the-session-store>`_.
* ``cas_clean_tickets``: old tickets and timed-out tickets do not get purge from
the database automatically. They are just marked as invalid. ``cas_clean_tickets``
is a clean-up management command for this purpose. It send SingleLogOut request
to services with timed out tickets and delete them.
* ``cas_clean_sessions``: Logout and purge users (sending SLO requests) that are
inactive since more than ``SESSION_COOKIE_AGE``. The default value for is ``1209600``
seconds (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day).
* ``clearsessions``: please see `Clearing the session store <https://docs.djangoproject.com/en/stable/topics/http/sessions/#clearing-the-session-store>`_.
* ``cas_clean_tickets``: old tickets and timed-out tickets do not get purge from
the database automatically. They are just marked as invalid. ``cas_clean_tickets``
is a clean-up management command for this purpose. It send SingleLogOut request
to services with timed out tickets and delete them.
* ``cas_clean_sessions``: Logout and purge users (sending SLO requests) that are
inactive since more than ``SESSION_COOKIE_AGE``. The default value for is ``1209600``
seconds (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day).
You could for example do as bellow :
You could for example do as bellow :
.. code-block::
......@@ -102,11 +102,11 @@ Quick start
*/5 * * * * cas-user /path/to/project/manage.py cas_clean_tickets
5 0 * * * cas-user /path/to/project/manage.py cas_clean_sessions
5. Start the development server and visit http://127.0.0.1:8000/admin/
6. Start the development server and visit http://127.0.0.1:8000/admin/
to add a first service allowed to authenticate user agains the CAS
(you'll need the Admin app enabled).
6. Visit http://127.0.0.1:8000/cas/ to login with your django users.
7. Visit http://127.0.0.1:8000/cas/ to login with your django users.
......@@ -138,7 +138,7 @@ Template settings:
Authentication settings:
* ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing
``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"``
``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"``
* ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
......@@ -217,15 +217,15 @@ Mysql backend settings. Only usefull if you are using the mysql authentication b
The default is ``"SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s"``
* ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following:
* ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database
should begin this $
* ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html)
the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256},
{SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}.
* ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512.
The hashed password in the database is compare to the hexadecimal digest of the clear
password hashed with the corresponding algorithm.
* ``"plain"``, the password in the database must be in clear.
* ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database
should begin this $
* ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html)
the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256},
{SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}.
* ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512.
The hashed password in the database is compare to the hexadecimal digest of the clear
password hashed with the corresponding algorithm.
* ``"plain"``, the password in the database must be in clear.
The default is ``"crypt"``.
......
......@@ -7,6 +7,6 @@
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2015 Valentin Samir
# (c) 2015-2016 Valentin Samir
"""A django CAS server application"""
default_app_config = 'cas_server.apps.CasAppConfig'
......@@ -7,7 +7,7 @@
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2015 Valentin Samir
# (c) 2015-2016 Valentin Samir
"""module for the admin interface of the app"""
from django.contrib import admin
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern
......
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
# more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2015-2016 Valentin Samir
"""django config module"""
from django.utils.translation import ugettext_lazy as _
from django.apps import AppConfig
class CasAppConfig(AppConfig):
"""django CAS application config class"""
name = 'cas_server'
verbose_name = _('Central Authentication Service')
......@@ -8,7 +8,7 @@
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2015 Valentin Samir
# (c) 2015-2016 Valentin Samir
"""Some authentication classes for the CAS"""
from django.conf import settings
from django.contrib.auth import get_user_model
......@@ -26,6 +26,7 @@ from .models import FederatedUser
class AuthUser(object):
"""Authentication base class"""
def __init__(self, username):
self.username = username
......
......@@ -90,6 +90,8 @@ setting_default(
}
)
setting_default('CAS_ENABLE_AJAX_AUTH', False)
setting_default('CAS_FEDERATE', False)
# A dict of "provider suffix" -> (provider CAS server url, CAS version, verbose name)
setting_default('CAS_FEDERATE_PROVIDERS', {})
......
......@@ -7,7 +7,7 @@
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2015 Valentin Samir
# (c) 2015-2016 Valentin Samir
"""forms for the app"""
from .default_settings import settings
......@@ -19,6 +19,7 @@ import cas_server.models as models
class WarnForm(forms.Form):
"""Form used on warn page before emiting a ticket"""
service = forms.CharField(widget=forms.HiddenInput(), required=False)
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
gateway = forms.CharField(widget=forms.HiddenInput(), required=False)
......
"""Clean deleted sessions management command"""
from django.core.management.base import BaseCommand
from django.utils.translation import ugettext_lazy as _
......@@ -5,6 +6,7 @@ from ... import models
class Command(BaseCommand):
"""Clean deleted sessions"""
args = ''
help = _(u"Clean deleted sessions")
......
"""Clean old trickets management command"""
from django.core.management.base import BaseCommand
from django.utils.translation import ugettext_lazy as _
......@@ -5,6 +6,7 @@ from ... import models
class Command(BaseCommand):
"""Clean old trickets"""
args = ''
help = _(u"Clean old trickets")
......
......@@ -8,7 +8,7 @@
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2015 Valentin Samir
# (c) 2015-2016 Valentin Samir
"""models for the app"""
from .default_settings import settings
......@@ -20,7 +20,6 @@ from django.utils import timezone
from picklefield.fields import PickledObjectField
import re
import os
import sys
import logging
from importlib import import_module
......@@ -79,6 +78,7 @@ class User(models.Model):
@classmethod
def clean_old_entries(cls):
"""Remove users inactive since more that SESSION_COOKIE_AGE"""
users = cls.objects.filter(
date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE))
)
......@@ -88,6 +88,7 @@ class User(models.Model):
@classmethod
def clean_deleted_sessions(cls):
"""Remove user where the session do not exists anymore"""
for user in cls.objects.all():
if not SessionStore(session_key=user.session_key).get('authenticated'):
user.logout()
......@@ -112,10 +113,10 @@ class User(models.Model):
for ticket_class in ticket_classes:
queryset = ticket_class.objects.filter(user=self)
for ticket in queryset:
ticket.logout(request, session, async_list)
ticket.logout(session, async_list)
queryset.delete()
for future in async_list:
if future:
if future: # pragma: no branch (should always be true)
try:
future.result()
except Exception as error:
......@@ -143,13 +144,21 @@ class User(models.Model):
(a.name, a.replace if a.replace else a.name) for a in service_pattern.attributs.all()
)
replacements = dict(
(a.name, (a.pattern, a.replace)) for a in service_pattern.replacements.all()
(a.attribut, (a.pattern, a.replace)) for a in service_pattern.replacements.all()
)
service_attributs = {}
for (key, value) in self.attributs.items():
if key in attributs or '*' in attributs:
if key in replacements:
value = re.sub(replacements[key][0], replacements[key][1], value)
if isinstance(value, list):
for index, subval in enumerate(value):
value[index] = re.sub(
replacements[key][0],
replacements[key][1],
subval
)
else:
value = re.sub(replacements[key][0], replacements[key][1], value)
service_attributs[attributs.get(key, key)] = value
ticket = ticket_class.objects.create(
user=self,
......@@ -173,6 +182,7 @@ class User(models.Model):
class ServicePatternException(Exception):
"""Base exception of exceptions raised in the ServicePattern model"""
pass
......@@ -426,77 +436,57 @@ class Ticket(models.Model):
).delete()
# sending SLO to timed-out validated tickets
if cls.TIMEOUT and cls.TIMEOUT > 0:
async_list = []
session = FuturesSession(
executor=ThreadPoolExecutor(max_workers=settings.CAS_SLO_MAX_PARALLEL_REQUESTS)
)
queryset = cls.objects.filter(
creation__lt=(timezone.now() - timedelta(seconds=cls.TIMEOUT))
)
for ticket in queryset:
ticket.logout(None, session, async_list)
queryset.delete()
for future in async_list:
if future:
try:
future.result()
except Exception as error:
logger.warning("Error durring SLO %s" % error)
sys.stderr.write("%r\n" % error)
def logout(self, request, session, async_list=None):
async_list = []
session = FuturesSession(
executor=ThreadPoolExecutor(max_workers=settings.CAS_SLO_MAX_PARALLEL_REQUESTS)
)
queryset = cls.objects.filter(
creation__lt=(timezone.now() - timedelta(seconds=cls.TIMEOUT))
)
for ticket in queryset:
ticket.logout(session, async_list)
queryset.delete()
for future in async_list:
if future: # pragma: no branch (should always be true)
try:
future.result()
except Exception as error:
logger.warning("Error durring SLO %s" % error)
sys.stderr.write("%r\n" % error)
def logout(self, session, async_list=None):
"""Send a SLO request to the ticket service"""
# On logout invalidate the Ticket
self.validate = True
self.save()
if self.validate and self.single_log_out:
if self.validate and self.single_log_out: # pragma: no branch (should always be true)
logger.info(
"Sending SLO requests to service %s for user %s" % (
self.service,
self.user.username
)
)
try:
xml = u"""<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="%(id)s" Version="2.0" IssueInstant="%(datetime)s">
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:NameID>
<samlp:SessionIndex>%(ticket)s</samlp:SessionIndex>
</samlp:LogoutRequest>""" % \
{
'id': os.urandom(20).encode("hex"),
'datetime': timezone.now().isoformat(),
'ticket': self.value
}
if self.service_pattern.single_log_out_callback:
url = self.service_pattern.single_log_out_callback
else:
url = self.service
async_list.append(
session.post(
url.encode('utf-8'),
data={'logoutRequest': xml.encode('utf-8')},
timeout=settings.CAS_SLO_TIMEOUT
)
)
except Exception as error:
error = utils.unpack_nested_exception(error)
logger.warning(
"Error durring SLO for user %s on service %s: %s" % (
self.user.username,
self.service,
error
)
xml = u"""<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="%(id)s" Version="2.0" IssueInstant="%(datetime)s">
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:NameID>
<samlp:SessionIndex>%(ticket)s</samlp:SessionIndex>
</samlp:LogoutRequest>""" % \
{
'id': utils.gen_saml_id(),
'datetime': timezone.now().isoformat(),
'ticket': self.value
}
if self.service_pattern.single_log_out_callback:
url = self.service_pattern.single_log_out_callback
else:
url = self.service
async_list.append(
session.post(
url.encode('utf-8'),
data={'logoutRequest': xml.encode('utf-8')},
timeout=settings.CAS_SLO_TIMEOUT
)
if request is not None:
messages.add_message(
request,
messages.WARNING,
_(u'Error during service logout %(service)s:\n%(error)s') %
{'service': self.service, 'error': error}
)
else:
sys.stderr.write("%r\n" % error)
)
class ServiceTicket(Ticket):
......
This diff is collapsed.
# ⁻*- coding: utf-8 -*-
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
# more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2016 Valentin Samir
"""Some mixin classes for tests"""
from cas_server.default_settings import settings
from django.utils import timezone
import re
from lxml import etree
from datetime import timedelta
from cas_server import models
from cas_server.tests.utils import get_auth_client
class BaseServicePattern(object):
"""Mixing for setting up service pattern for testing"""
def setup_service_patterns(self, proxy=False):
"""setting up service pattern"""
# For general purpose testing
self.service = "https://www.example.com"
self.service_pattern = models.ServicePattern.objects.create(
name="example",
pattern="^https://www\.example\.com(/.*)?$",
proxy=proxy,
)
models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern)
# For testing the restrict_users attributes
self.service_restrict_user_fail = "https://restrict_user_fail.example.com"
self.service_pattern_restrict_user_fail = models.ServicePattern.objects.create(
name="restrict_user_fail",
pattern="^https://restrict_user_fail\.example\.com(/.*)?$",
restrict_users=True,
proxy=proxy,
)
self.service_restrict_user_success = "https://restrict_user_success.example.com"
self.service_pattern_restrict_user_success = models.ServicePattern.objects.create(
name="restrict_user_success",
pattern="^https://restrict_user_success\.example\.com(/.*)?$",
restrict_users=True,
proxy=proxy,
)
models.Username.objects.create(
value=settings.CAS_TEST_USER,
service_pattern=self.service_pattern_restrict_user_success
)
# For testing the user attributes filtering conditions
self.service_filter_fail = "https://filter_fail.example.com"
self.service_pattern_filter_fail = models.ServicePattern.objects.create(
name="filter_fail",
pattern="^https://filter_fail\.example\.com(/.*)?$",
proxy=proxy,
)
models.FilterAttributValue.objects.create(
attribut="right",
pattern="^admin$",
service_pattern=self.service_pattern_filter_fail
)
self.service_filter_fail_alt = "https://filter_fail_alt.example.com"
self.service_pattern_filter_fail_alt = models.ServicePattern.objects.create(
name="filter_fail_alt",
pattern="^https://filter_fail_alt\.example\.com(/.*)?$",
proxy=proxy,
)
models.FilterAttributValue.objects.create(
attribut="nom",
pattern="^toto$",
service_pattern=self.service_pattern_filter_fail_alt
)
self.service_filter_success = "https://filter_success.example.com"
self.service_pattern_filter_success = models.ServicePattern.objects.create(
name="filter_success",
pattern="^https://filter_success\.example\.com(/.*)?$",
proxy=proxy,
)
models.FilterAttributValue.objects.create(
attribut="email",
pattern="^%s$" % re.escape(settings.CAS_TEST_ATTRIBUTES['email']),
service_pattern=self.service_pattern_filter_success
)
# For testing the user_field attributes
self.service_field_needed_fail = "https://field_needed_fail.example.com"
self.service_pattern_field_needed_fail = models.ServicePattern.objects.create(
name="field_needed_fail",
pattern="^https://field_needed_fail\.example\.com(/.*)?$",
user_field="uid",
proxy=proxy,
)
self.service_field_needed_success = "https://field_needed_success.example.com"
self.service_pattern_field_needed_success = models.ServicePattern.objects.create(
name="field_needed_success",
pattern="^https://field_needed_success\.example\.com(/.*)?$",
user_field="alias",
proxy=proxy,
)
self.service_field_needed_success_alt = "https://field_needed_success_alt.example.com"
self.service_pattern_field_needed_success = models.ServicePattern.objects.create(
name="field_needed_success_alt",
pattern="^https://field_needed_success_alt\.example\.com(/.*)?$",
user_field="nom",
proxy=proxy,
)
class XmlContent(object):
"""Mixin for test on CAS XML responses"""
def assert_error(self, response, code, text=None):
"""Assert a validation error"""
self.assertEqual(response.status_code, 200)
root = etree.fromstring(response.content)
error = root.xpath(
"//cas:authenticationFailure",
namespaces={'cas': "http://www.yale.edu/tp/cas"}
)
self.assertEqual(len(error), 1)
self.assertEqual(error[0].attrib['code'], code)
if text is not None:
self.assertEqual(error[0].text, text)
def assert_success(self, response, username, original_attributes):
"""assert a ticket validation success"""
self.assertEqual(response.status_code, 200)
root = etree.fromstring(response.content)
sucess = root.xpath(
"//cas:authenticationSuccess",
namespaces={'cas': "http://www.yale.edu/tp/cas"}
)
self.assertTrue(sucess)
users = root.xpath("//cas:user", namespaces={'cas': "http://www.yale.edu/tp/cas"})
self.assertEqual(len(users), 1)
self.assertEqual(users[0].text, username)
attributes = root.xpath(
"//cas:attributes",
namespaces={'cas': "http://www.yale.edu/tp/cas"}
)
self.assertEqual(len(attributes), 1)
attrs1 = set()
for attr in attributes[0]:
attrs1.add((attr.tag[len("http://www.yale.edu/tp/cas")+2:], attr.text))
attributes = root.xpath("//cas:attribute", namespaces={'cas': "http://www.yale.edu/tp/cas"})
self.assertEqual(len(attributes), len(attrs1))
attrs2 = set()
for attr in attributes:
attrs2.add((attr.attrib['name'], attr.attrib['value']))
original = set()
for key, value in original_attributes.items():
if isinstance(value, list):
for sub_value in value:
original.add((key, sub_value))
else:
original.add((key, value))
self.assertEqual(attrs1, attrs2)
self.assertEqual(attrs1, original)
return root
class UserModels(object):
"""Mixin for test on CAS user models"""
@staticmethod
def expire_user():
"""return an expired user"""
client = get_auth_client()
new_date = timezone.now() - timedelta(seconds=(settings.SESSION_COOKIE_AGE + 600))
models.User.objects.filter(
username=settings.CAS_TEST_USER,
session_key=client.session.session_key
).update(date=new_date)
return client