[Python-modules-commits] [python-django-casclient] 01/02: Imported Upstream version 1.2.0

Joost van Baal joostvb at moszumanska.debian.org
Tue Mar 15 09:03:33 UTC 2016


This is an automated email from the git hooks/post-receive script.

joostvb pushed a commit to branch master
in repository python-django-casclient.

commit 44a0492246fb5ce7f71aba1fb3c2966fb86e11c2
Author: Joost van Baal-Ilić <joostvb at janacopoulos.mdcc.cx>
Date:   Tue Mar 15 10:02:58 2016 +0100

    Imported Upstream version 1.2.0
---
 .gitignore                     |  31 +++++
 .travis.yml                    |  22 ++++
 AUTHORS                        |   6 +
 CHANGELOG.md                   |  11 ++
 CONTRIBUTORS                   |   4 +
 LICENSE.mit                    |  22 ++++
 MANIFEST.in                    |   6 +
 README.md                      | 158 +++++++++++++++++++++++
 cas/__init__.py                |  32 +++++
 cas/backends.py                | 257 ++++++++++++++++++++++++++++++++++++
 cas/decorators.py              |  93 +++++++++++++
 cas/exceptions.py              |  17 +++
 cas/middleware.py              |  92 +++++++++++++
 cas/models.py                  | 108 ++++++++++++++++
 cas/tests/__init__.py          |   3 +
 cas/tests/factories.py         |  15 +++
 cas/tests/test_backend.py      |  30 +++++
 cas/tests/test_smoke.py        |   7 +
 cas/tests/test_views.py        |  46 +++++++
 cas/utils.py                   |  26 ++++
 cas/views.py                   | 257 ++++++++++++++++++++++++++++++++++++
 docs/Makefile                  | 192 +++++++++++++++++++++++++++
 docs/source/changelog.rst      |  21 +++
 docs/source/conf.py            | 287 +++++++++++++++++++++++++++++++++++++++++
 docs/source/contributing.rst   |  12 ++
 docs/source/gettingstarted.rst | 133 +++++++++++++++++++
 docs/source/index.rst          |  27 ++++
 requirements-dev.txt           |   1 +
 run_tests.py                   |  59 +++++++++
 setup.cfg                      |   2 +
 setup.py                       |  41 ++++++
 31 files changed, 2018 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..835e4ac
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,31 @@
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
+.idea
+
+# testing
+test_config.py
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a18521b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,22 @@
+language: python
+
+python:
+  - "2.7"
+  - "3.3"
+  - "3.4"
+
+env:
+  - DJANGO_VERSION=Django==1.5
+  - DJANGO_VERSION=Django==1.6
+  - DJANGO_VERSION=Django==1.7
+  - DJANGO_VERSION=Django==1.8
+
+# command to install dependencies
+install:
+  - pip install $DJANGO_VERSION
+  - pip install -r requirements-dev.txt
+
+# command to run tests
+
+script:
+  - python run_tests.py
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..1b63dff
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,6 @@
+This fork of Django-CAS was originally created at https://bitbucket.org/cpcc/django-cas/
+
+The authors maintainers of this library are:
+
+    * Garrett Pennington
+    * Derek Stegelman
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..fa3f0b2
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+### 1.2.0
+
+- Allow opt out of time delay caused by fetching PGT tickets
+- Add support for gateway not returning a response
+- Allow forcing service URL over HTTPS (https://github.com/kstateome/django-cas/pull/48)
+- Allow user creation on first login to be optional (https://github.com/kstateome/django-cas/pull/49)
+
+### 1.1.1
+
+- Add a few logging statements
+- Add official change log.
\ No newline at end of file
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
new file mode 100644
index 0000000..7a2b681
--- /dev/null
+++ b/CONTRIBUTORS
@@ -0,0 +1,4 @@
+epicserve
+rlmv
+bryankaplan
+cordmata
\ No newline at end of file
diff --git a/LICENSE.mit b/LICENSE.mit
new file mode 100644
index 0000000..37f7704
--- /dev/null
+++ b/LICENSE.mit
@@ -0,0 +1,22 @@
+Copyright (c) 2013 Office of Mediated Education, Kansas State University
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..ab70df3
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+include LICENSE.mit
+include README.md
+include CHANGELOG.md
+include AUTHORS
+include CONTRIBUTORS
+recursive-exclude * *.pyc
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2375726
--- /dev/null
+++ b/README.md
@@ -0,0 +1,158 @@
+# django-cas
+
+CAS client for Django.  This library requires Django 1.5 or above, and Python 2.6, 2.7, 3.4
+
+Current version: 1.2.0
+
+This is [K-State's fork](https://github.com/kstateome/django-cas) of [the original](https://bitbucket.org/cpcc/django-cas/overview) and includes [several additional features](https://github.com/kstateome/django-cas/#additional-features) as well as features merged from
+
+*  [KTHse's django-cas2](https://github.com/KTHse/django-cas2).
+*  [Edmund Crewe's proxy ticket patch](http://code.google.com/r/edmundcrewe-proxypatch/source/browse/django-cas-proxy.patch).
+
+
+## Install
+
+This project is registered on PyPi as django-cas-client.  To install::
+
+    pip install django-cas-client==1.2.0
+
+
+### Add to URLs
+
+Add the login and logout patterns to your main URLS conf.
+
+    # CAS
+    url(r'^accounts/login/$', 'cas.views.login', name='login'),
+    url(r'^accounts/logout/$', 'cas.views.logout', name='logout'),
+
+### Add middleware and settings
+
+Set your CAS server URL
+
+    CAS_SERVER_URL = "https://signin.somehwere/cas/"
+
+Add cas to middleware classes
+
+    'cas.middleware.CASMiddleware',
+
+
+### Add authentication backends
+
+    AUTHENTICATION_BACKENDS = (
+        'django.contrib.auth.backends.ModelBackend',
+        'cas.backends.CASBackend',
+    )
+
+## How to Contribute
+
+Fork and branch off of the ``develop`` branch.  Submit Pull requests back to ``kstateome:develop``.
+
+### Run The Tests
+
+All PRs must pass unit tests.  To run the tests locally:
+
+    pip install -r requirements-dev.txt
+    python run_tests.py
+
+
+## Settings.py for CAS
+
+Add the following to middleware if you want to use CAS::
+
+    MIDDLEWARE_CLASSES = (
+    'cas.middleware.CASMiddleware',
+    )
+
+
+Add these to ``settings.py`` to use the CAS Backend::
+
+
+    CAS_SERVER_URL = "Your Cas Server"
+    CAS_LOGOUT_COMPLETELY = True
+    CAS_PROVIDE_URL_TO_LOGOUT = True
+
+# Additional Features
+
+This fork contains additional features not found in the original:
+*  Proxied Hosts
+*  CAS Response Callbacks
+*  CAS Gateway
+*  Proxy Tickets (From Edmund Crewe)
+
+## Proxied Hosts
+
+You will need to setup middleware to handle the use of proxies.
+
+Add a setting ``PROXY_DOMAIN`` of the domain you want the client to use.  Then add
+
+    MIDDLEWARE_CLASSES = (
+    'cas.middleware.ProxyMiddleware',
+    )
+
+This middleware needs to be added before the django ``common`` middleware.
+
+
+## CAS Response Callbacks
+
+To store data from CAS, create a callback function that accepts the ElementTree object from the
+proxyValidate response. There can be multiple callbacks, and they can live anywhere. Define the
+callback(s) in ``settings.py``:
+
+    CAS_RESPONSE_CALLBACKS = (
+        'path.to.module.callbackfunction',
+        'anotherpath.to.module.callbackfunction2',
+    )
+
+and create the functions in ``path/to/module.py``:
+
+    def callbackfunction(tree):
+        username = tree[0][0].text
+
+        user, user_created = User.objects.get_or_create(username=username)
+        profile, created = user.get_profile()
+
+        profile.email = tree[0][1].text
+        profile.position = tree[0][2].text
+        profile.save()
+
+
+## CAS Gateway
+
+To use the CAS Gateway feature, first enable it in settings. Trying to use it without explicitly
+enabling this setting will raise an ImproperlyConfigured:
+
+    CAS_GATEWAY = True
+
+Then, add the ``gateway`` decorator to a view:
+
+    from cas.decorators import gateway
+
+    @gateway()
+    def foo(request):
+        #stuff
+        return render(request, 'foo/bar.html')
+
+
+## Custom Forbidden Page
+
+To show a custom forbidden page, set ``CAS_CUSTOM_FORBIDDEN`` to a ``path.to.some_view``.  Otherwise,
+a generic ``HttpResponseForbidden`` will be returned.
+
+## Require SSL Login
+
+To force the service url to always target HTTPS, set ``CAS_FORCE_SSL_SERVICE_URL`` to ``True``.
+
+## Automatically Create Users on First Login
+
+By default, a stub user record will be created on the first successful CAS authentication
+using the username in the response. If this behavior is not desired set
+``CAS_AUTO_CREATE_USER`` to ``Flase``.
+
+## Proxy Tickets
+
+This fork also includes
+[Edmund Crewe's proxy ticket patch](http://code.google.com/r/edmundcrewe-proxypatch/source/browse/django-cas-proxy.patch).
+
+You can opt out of the time delay sometimes caused by proxy ticket validation by setting:
+
+    CAS_PGT_FETCH_WAIT = False
diff --git a/cas/__init__.py b/cas/__init__.py
new file mode 100644
index 0000000..59ea0eb
--- /dev/null
+++ b/cas/__init__.py
@@ -0,0 +1,32 @@
+"""Django CAS 1.0/2.0 authentication backend"""
+
+from django.conf import settings
+
+__all__ = []
+
+_DEFAULTS = {
+    'CAS_ADMIN_PREFIX': None,
+    'CAS_EXTRA_LOGIN_PARAMS': None,
+    'CAS_IGNORE_REFERER': False,
+    'CAS_LOGOUT_COMPLETELY': True,
+    'CAS_REDIRECT_URL': '/',
+    'CAS_RETRY_LOGIN': False,
+    'CAS_SERVER_URL': None,
+    'CAS_VERSION': '2',
+    'CAS_GATEWAY': False,
+    'CAS_PROXY_CALLBACK': None,
+    'CAS_RESPONSE_CALLBACKS': None,
+    'CAS_CUSTOM_FORBIDDEN': None,
+    'CAS_PGT_FETCH_WAIT': True,
+    'CAS_FORCE_SSL_SERVICE_URL': False,
+    'CAS_AUTO_CREATE_USER': True,
+}
+
+for key, value in _DEFAULTS.items():
+    try:
+        getattr(settings, key)
+    except AttributeError:
+        setattr(settings, key, value)
+    # Suppress errors from DJANGO_SETTINGS_MODULE not being set
+    except ImportError:
+        pass
diff --git a/cas/backends.py b/cas/backends.py
new file mode 100644
index 0000000..5a646aa
--- /dev/null
+++ b/cas/backends.py
@@ -0,0 +1,257 @@
+import logging
+from xml.dom import minidom
+import time
+
+try:
+    from xml.etree import ElementTree
+except ImportError:
+    from elementtree import ElementTree
+
+try:
+    from urllib import urlencode
+except ImportError:
+    from urllib.parse import urlencode
+try:
+    from urllib import urlopen
+except ImportError:
+    from urllib.request import urlopen
+try:
+    from urlparse import urljoin
+except ImportError:
+    from urllib.parse import urljoin
+
+
+from django.conf import settings
+from django.contrib.auth import get_user_model
+
+from cas.exceptions import CasTicketException
+from cas.models import Tgt, PgtIOU
+from cas.utils import cas_response_callbacks
+
+__all__ = ['CASBackend']
+
+logger = logging.getLogger(__name__)
+
+
+def _verify_cas1(ticket, service):
+    """
+    Verifies CAS 1.0 authentication ticket.
+
+    :param: ticket
+    :param: service
+
+    Returns username on success and None on failure.
+    """
+
+    params = {'ticket': ticket, 'service': service}
+    url = (urljoin(settings.CAS_SERVER_URL, 'validate') + '?' +
+           urlencode(params))
+    page = urlopen(url)
+
+    try:
+        verified = page.readline().strip()
+        if verified == 'yes':
+            return page.readline().strip()
+        else:
+            return None
+    finally:
+        page.close()
+
+
+def _verify_cas2(ticket, service):
+    """
+    Verifies CAS 2.0+ XML-based authentication ticket.
+
+    :param: ticket
+    :param: service
+    """
+    return _internal_verify_cas(ticket, service, 'proxyValidate')
+
+
+def _verify_cas3(ticket, service):
+    return _internal_verify_cas(ticket, service, 'p3/proxyValidate')
+
+
+def _internal_verify_cas(ticket, service, suffix):
+    """Verifies CAS 2.0 and 3.0 XML-based authentication ticket.
+
+    Returns username on success and None on failure.
+    """
+
+    params = {'ticket': ticket, 'service': service}
+    if settings.CAS_PROXY_CALLBACK:
+        params['pgtUrl'] = settings.CAS_PROXY_CALLBACK
+
+    url = (urljoin(settings.CAS_SERVER_URL, suffix) + '?' +
+           urlencode(params))
+
+    page = urlopen(url)
+
+    username = None
+
+    try:
+        response = page.read()
+        tree = ElementTree.fromstring(response)
+        document = minidom.parseString(response)
+
+        if tree[0].tag.endswith('authenticationSuccess'):
+            if settings.CAS_RESPONSE_CALLBACKS:
+                cas_response_callbacks(tree)
+
+            username = tree[0][0].text
+
+            pgt_el = document.getElementsByTagName('cas:proxyGrantingTicket')
+
+            if pgt_el:
+                pgt = pgt_el[0].firstChild.nodeValue
+                try:
+                    pgtIou = _get_pgtiou(pgt)
+                    tgt = Tgt.objects.get(username=username)
+                    tgt.tgt = pgtIou.tgt
+                    tgt.save()
+                    pgtIou.delete()
+                except Tgt.DoesNotExist:
+                    Tgt.objects.create(username=username, tgt=pgtIou.tgt)
+                    logger.info('Creating TGT ticket for {user}'.format(
+                        user=username
+                    ))
+                    pgtIou.delete()
+                except Exception as e:
+                    logger.warning('Failed to do proxy authentication. {message}'.format(
+                        message=e
+                    ))
+
+        else:
+            failure = document.getElementsByTagName('cas:authenticationFailure')
+            if failure:
+                logger.warn('Authentication failed from CAS server: %s',
+                            failure[0].firstChild.nodeValue)
+
+    except Exception as e:
+        logger.error('Failed to verify CAS authentication: {message}'.format(
+            message=e
+        ))
+
+    finally:
+        page.close()
+
+    return username
+
+
+def verify_proxy_ticket(ticket, service):
+    """
+    Verifies CAS 2.0+ XML-based proxy ticket.
+
+    :param: ticket
+    :param: service
+
+    Returns username on success and None on failure.
+    """
+
+    params = {'ticket': ticket, 'service': service}
+
+    url = (urljoin(settings.CAS_SERVER_URL, 'proxyValidate') + '?' +
+           urlencode(params))
+
+    page = urlopen(url)
+
+    try:
+        response = page.read()
+        tree = ElementTree.fromstring(response)
+        if tree[0].tag.endswith('authenticationSuccess'):
+            username = tree[0][0].text
+            proxies = []
+            if len(tree[0]) > 1:
+                for element in tree[0][1]:
+                    proxies.append(element.text)
+            return {"username": username, "proxies": proxies}
+        else:
+            return None
+    finally:
+        page.close()
+
+_PROTOCOLS = {'1': _verify_cas1, '2': _verify_cas2, '3': _verify_cas3}
+
+if settings.CAS_VERSION not in _PROTOCOLS:
+    raise ValueError('Unsupported CAS_VERSION %r' % settings.CAS_VERSION)
+
+_verify = _PROTOCOLS[settings.CAS_VERSION]
+
+
+def _get_pgtiou(pgt):
+    """
+    Returns a PgtIOU object given a pgt.
+
+    The PgtIOU (tgt) is set by the CAS server in a different request
+    that has completed before this call, however, it may not be found in
+    the database by this calling thread, hence the attempt to get the
+    ticket is retried for up to 5 seconds. This should be handled some
+    better way.
+
+    Users can opt out of this waiting period by setting CAS_PGT_FETCH_WAIT = False
+
+    :param: pgt
+
+    """
+
+    pgtIou = None
+    retries_left = 5
+
+    if not settings.CAS_PGT_FETCH_WAIT:
+        retries_left = 1
+
+    while not pgtIou and retries_left:
+        try:
+            return PgtIOU.objects.get(tgt=pgt)
+        except PgtIOU.DoesNotExist:
+            if settings.CAS_PGT_FETCH_WAIT:
+                time.sleep(1)
+            retries_left -= 1
+            logger.info('Did not fetch ticket, trying again.  {tries} tries left.'.format(
+                tries=retries_left
+            ))
+    raise CasTicketException("Could not find pgtIou for pgt %s" % pgt)
+
+
+class CASBackend(object):
+    """
+    CAS authentication backend
+    """
+
+    supports_object_permissions = False
+    supports_inactive_user = False
+
+    def authenticate(self, ticket, service):
+        """
+        Verifies CAS ticket and gets or creates User object
+        NB: Use of PT to identify proxy
+        """
+
+        User = get_user_model()
+        username = _verify(ticket, service)
+
+        if not username:
+            return None
+
+        try:
+            user = User.objects.get(username__iexact=username)
+        except User.DoesNotExist:
+            # user will have an "unusable" password
+            if settings.CAS_AUTO_CREATE_USER:
+                user = User.objects.create_user(username, '')
+                user.save()
+            else:
+                user = None
+        return user
+
+    def get_user(self, user_id):
+        """
+        Retrieve the user's entry in the User model if it exists
+        """
+
+        User = get_user_model()
+
+        try:
+            return User.objects.get(pk=user_id)
+        except User.DoesNotExist:
+            return None
diff --git a/cas/decorators.py b/cas/decorators.py
new file mode 100644
index 0000000..7cc0e3c
--- /dev/null
+++ b/cas/decorators.py
@@ -0,0 +1,93 @@
+try:
+    from functools import wraps
+except ImportError:
+    from django.utils.functional import wraps
+
+try:
+    from urllib import urlencode
+except ImportError:
+    from urllib.parse import urlencode
+
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.http import HttpResponseForbidden, HttpResponseRedirect
+from django.utils.http import urlquote
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+
+__all__ = ['permission_required', 'user_passes_test']
+
+
+def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
+    """
+    Replacement for django.contrib.auth.decorators.user_passes_test that
+    returns 403 Forbidden if the user is already logged in.
+    """
+
+    if not login_url:
+        login_url = settings.LOGIN_URL
+
+    def decorator(view_func):
+        @wraps(view_func)
+        def wrapper(request, *args, **kwargs):
+            if test_func(request.user):
+                return view_func(request, *args, **kwargs)
+            elif request.user.is_authenticated():
+                return HttpResponseForbidden('<h1>Permission denied</h1>')
+            else:
+                path = '%s?%s=%s' % (login_url, redirect_field_name,
+                                     urlquote(request.get_full_path()))
+                return HttpResponseRedirect(path)
+        return wrapper
+    return decorator
+
+
+def permission_required(perm, login_url=None):
+    """
+    Replacement for django.contrib.auth.decorators.permission_required that
+    returns 403 Forbidden if the user is already logged in.
+    """
+
+    return user_passes_test(lambda u: u.has_perm(perm), login_url=login_url)
+
+
+def gateway():
+    """
+    Authenticates single sign on session if ticket is available,
+    but doesn't redirect to sign in url otherwise.
+    """
+
+    if settings.CAS_GATEWAY == False:
+        raise ImproperlyConfigured('CAS_GATEWAY must be set to True')
+
+    def wrap(func):
+        def wrapped_f(*args):
+
+            from cas.views import login
+            request = args[0]
+
+            if request.user.is_authenticated():
+                # Is Authed, fine
+                pass
+            else:
+                path_with_params = request.path + '?' + urlencode(request.GET.copy())
+                if request.GET.get('ticket'):
+                    # Not Authed, but have a ticket!
+                    # Try to authenticate
+                    response = login(request, path_with_params, False, True)
+                    if isinstance(response, HttpResponseRedirect):
+                        # For certain instances where a forbidden occurs, we need to pass instead of return a response.
+                        return response
+                else:
+                    #Not Authed, but no ticket
+                    gatewayed = request.GET.get('gatewayed')
+                    if gatewayed == 'true':
+                        pass
+                    else:
+                        # Not Authed, try to authenticate
+                        response = login(request, path_with_params, False, True)
+                        if isinstance(response, HttpResponseRedirect):
+                            return response
+
+            return func(*args)
+        return wrapped_f
+    return wrap
diff --git a/cas/exceptions.py b/cas/exceptions.py
new file mode 100644
index 0000000..c50d3c2
--- /dev/null
+++ b/cas/exceptions.py
@@ -0,0 +1,17 @@
+from django.core.exceptions import ValidationError
+
+
+class CasTicketException(ValidationError):
+    """
+    The ticket fails to validate
+    """
+
+    pass
+
+
+class CasConfigException(ValidationError):
+    """
+    The config is wrong
+    """
+
+    pass
diff --git a/cas/middleware.py b/cas/middleware.py
new file mode 100644
index 0000000..771a1d1
--- /dev/null
+++ b/cas/middleware.py
@@ -0,0 +1,92 @@
+"""CAS authentication middleware"""
+
+try:
+    from urllib import urlencode
+except ImportError:
+    from urllib.parse import urlencode
+
+from django.conf import settings
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth import logout as do_logout
+from django.contrib.auth.views import login, logout
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect, HttpResponseForbidden
+from django.core.exceptions import ImproperlyConfigured
+
+from cas.exceptions import CasTicketException
+from cas.views import login as cas_login, logout as cas_logout
+
+__all__ = ['CASMiddleware']
+
+
+class CASMiddleware(object):
+    """
+    Middleware that allows CAS authentication on admin pages
+    """
+
+    def process_request(self, request):
+        """
+        Checks that the authentication middleware is installed
+
+        :param: request
+
+        """
+
+        error = ("The Django CAS middleware requires authentication "
+                 "middleware to be installed. Edit your MIDDLEWARE_CLASSES "
+                 "setting to insert 'django.contrib.auth.middleware."
+                 "AuthenticationMiddleware'.")
+        assert hasattr(request, 'user'), error
+
+    def process_view(self, request, view_func, view_args, view_kwargs):
+        """
+        Forwards unauthenticated requests to the admin page to the CAS
+        login URL, as well as calls to django.contrib.auth.views.login and
+        logout.
+        """
+
+        if view_func == login:
+            return cas_login(request, *view_args, **view_kwargs)
+        elif view_func == logout:
+            return cas_logout(request, *view_args, **view_kwargs)
+
+        if settings.CAS_ADMIN_PREFIX:
+            if not request.path.startswith(settings.CAS_ADMIN_PREFIX):
+                return None
+        elif not view_func.__module__.startswith('django.contrib.admin.'):
+            return None
+
+        if request.user.is_authenticated():
+            if request.user.is_staff:
+                return None
+            else:
+                error = ('<h1>Forbidden</h1><p>You do not have staff '
+                         'privileges.</p>')
+                return HttpResponseForbidden(error)
+
+        params = urlencode({REDIRECT_FIELD_NAME: request.get_full_path()})
+        return HttpResponseRedirect(reverse(cas_login) + '?' + params)
+
+    def process_exception(self, request, exception):
+        """
+        When we get a CasTicketException, that is probably caused by the ticket timing out.
+        So logout/login and get the same page again.
+        """
+
+        if isinstance(exception, CasTicketException):
+            do_logout(request)
+            # This assumes that request.path requires authentication.
+            return HttpResponseRedirect(request.path)
+        else:
+            return None
+
+
+class ProxyMiddleware(object):
+
+    # Middleware used to "fake" the django app that it lives at the Proxy Domain
+    def process_request(self, request):
+        proxy = getattr(settings, 'PROXY_DOMAIN', None)
+        if not proxy:
+            raise ImproperlyConfigured('To use Proxy Middleware you must set a PROXY_DOMAIN setting.')
+        else:
+            request.META['HTTP_HOST'] = proxy
diff --git a/cas/models.py b/cas/models.py
new file mode 100644
index 0000000..54b1606
--- /dev/null
+++ b/cas/models.py
@@ -0,0 +1,108 @@
+import logging
+from datetime import datetime
+try:
+    from xml.etree import ElementTree
+except ImportError:
+    from elementtree import ElementTree
+try:
+    from urlparse import urljoin
+except ImportError:
+    from urllib.parse import urljoin
+try:
+    from urllib import urlencode
+except ImportError:
+    from urllib.parse import urlencode
+try:
+    from urllib import urlopen
+except ImportError:
+    from urllib.request import urlopen
+
+from django.db import models
+from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models.signals import post_save
+
+
+from cas.exceptions import CasTicketException, CasConfigException
+
+
+logger = logging.getLogger(__name__)
+
+
+class Tgt(models.Model):
+    username = models.CharField(max_length=255, unique=True)
+    tgt = models.CharField(max_length=255)
+
+    def get_proxy_ticket_for(self, service):
+        """
+        Verifies CAS 2.0+ XML-based authentication ticket.
+
+        :param: service
+
+        Returns username on success and None on failure.
+        """
+
+        if not settings.CAS_PROXY_CALLBACK:
+            raise CasConfigException("No proxy callback set in settings")
+
+        params = {'pgt': self.tgt, 'targetService': service}
+
+        url = (urljoin(settings.CAS_SERVER_URL, 'proxy') + '?' +
+               urlencode(params))
+
+        page = urlopen(url)
+
+        try:
+            response = page.read()
+            tree = ElementTree.fromstring(response)
+            if tree[0].tag.endswith('proxySuccess'):
+                return tree[0][0].text
+            else:
+                logger.warning('Failed to get proxy ticket')
+                raise CasTicketException('Failed to get proxy ticket: %s' % \
+                                         tree[0].text.strip())
+        finally:
+            page.close()
+
+
+class PgtIOU(models.Model):
+    """
+    Proxy granting ticket and IOU
+    """
+    pgtIou = models.CharField(max_length = 255, unique = True)
+    tgt = models.CharField(max_length = 255)
+    created = models.DateTimeField(auto_now = True)
+
+
+def get_tgt_for(user):
+    """
+    Fetch a ticket granting ticket for a given user.
+
+    :param user: UserObj
+
+    :return: TGT or Exepction
+    """
+    if not settings.CAS_PROXY_CALLBACK:
+        raise CasConfigException("No proxy callback set in settings")
+
+    try:
+        return Tgt.objects.get(username=user.username)
+    except ObjectDoesNotExist:
+        logger.warning('No ticket found for user {user}'.format(
+            user=user.username
+        ))
+        raise CasTicketException("no ticket found for user " + user.username)
+
+
+def delete_old_tickets(**kwargs):
+    """
+    Delete tickets if they are over 2 days old
+    kwargs = ['raw', 'signal', 'instance', 'sender', 'created']
+
+    """
+    sender = kwargs.get('sender', None)
+    now = datetime.now()
+    expire = datetime(now.year, now.month, now.day - 2)
+    sender.objects.filter(created__lt=expire).delete()
+
+post_save.connect(delete_old_tickets, sender=PgtIOU)
diff --git a/cas/tests/__init__.py b/cas/tests/__init__.py
new file mode 100644
index 0000000..3b172cc
--- /dev/null
+++ b/cas/tests/__init__.py
@@ -0,0 +1,3 @@
+from cas.tests.test_smoke import *
+from cas.tests.test_backend import *
+from cas.tests.test_views import *
\ No newline at end of file
diff --git a/cas/tests/factories.py b/cas/tests/factories.py
new file mode 100644
index 0000000..865d348
--- /dev/null
... 1251 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django-casclient.git



More information about the Python-modules-commits mailing list