commit
f837853043
4
.gitignore
vendored
4
.gitignore
vendored
@ -4,12 +4,13 @@
|
||||
*.swp
|
||||
|
||||
build/
|
||||
bootstrap3
|
||||
cas/
|
||||
dist/
|
||||
db.sqlite3
|
||||
manage.py
|
||||
coverage.xml
|
||||
docs/_build/
|
||||
docs/django.inv
|
||||
|
||||
.tox
|
||||
test_venv
|
||||
@ -17,3 +18,4 @@ test_venv
|
||||
htmlcov/
|
||||
tox_logs/
|
||||
.cache/
|
||||
.eggs/
|
||||
|
37
.travis.yml
37
.travis.yml
@ -1,17 +1,28 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
env:
|
||||
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
|
||||
matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=coverage
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=flake8
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=check_rst
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=py27-django17
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=py27-django18
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=py27-django19
|
||||
- python: "3.4"
|
||||
env: TOX_ENV=py34-django17
|
||||
- python: "3.4"
|
||||
env: TOX_ENV=py34-django18
|
||||
- python: "3.4"
|
||||
env: TOX_ENV=py34-django19
|
||||
- python: "3.5"
|
||||
env: TOX_ENV=py35-django18
|
||||
- python: "3.5"
|
||||
env: TOX_ENV=py35-django19
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip/http/
|
||||
|
14
MANIFEST.in
14
MANIFEST.in
@ -1,7 +1,21 @@
|
||||
include tox.ini
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include .coveragerc
|
||||
include Makefile
|
||||
include pytest.ini
|
||||
include requirements-dev.txt
|
||||
include requirements.txt
|
||||
prune .tox
|
||||
recursive-include cas_server/static *
|
||||
recursive-include cas_server/templates *
|
||||
recursive-include cas_server/locale *
|
||||
|
||||
include docs/conf.py
|
||||
include docs/index.rst
|
||||
include docs/Makefile
|
||||
include docs/README.rst
|
||||
recursive-include docs/_ext *
|
||||
recursive-include docs/package *
|
||||
recursive-include docs/_static *
|
||||
recursive-include docs/_templates *
|
||||
|
19
Makefile
19
Makefile
@ -1,4 +1,4 @@
|
||||
.PHONY: build dist
|
||||
.PHONY: build dist docs
|
||||
VERSION=`python setup.py -V`
|
||||
|
||||
build:
|
||||
@ -24,10 +24,14 @@ clean_coverage:
|
||||
rm -rf coverage.xml .coverage htmlcov
|
||||
clean_tild_backup:
|
||||
find ./ -name '*~' -delete
|
||||
clean_docs:
|
||||
rm -rf docs/_build/ docs/django.inv
|
||||
clean_eggs:
|
||||
rm -rf .eggs/
|
||||
|
||||
clean: clean_pyc clean_build clean_coverage clean_tild_backup
|
||||
|
||||
clean_all: clean clean_tox clean_test_venv
|
||||
clean_all: clean clean_tox clean_test_venv clean_docs clean_eggs
|
||||
|
||||
dist:
|
||||
python setup.py sdist
|
||||
@ -40,7 +44,7 @@ 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
|
||||
sed -i "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n 'bootstrap3',\n 'cas_server',/" test_venv/cas/cas/settings.py
|
||||
sed -i "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n 'cas_server',/" test_venv/cas/cas/settings.py
|
||||
sed -i "s/'django.middleware.clickjacking.XFrameOptionsMiddleware',/'django.middleware.clickjacking.XFrameOptionsMiddleware',\n 'django.middleware.locale.LocaleMiddleware',/" test_venv/cas/cas/settings.py
|
||||
sed -i 's/from django.conf.urls import url/from django.conf.urls import url, include/' test_venv/cas/cas/urls.py
|
||||
sed -i "s@url(r'^admin/', admin.site.urls),@url(r'^admin/', admin.site.urls),\n url(r'^', include('cas_server.urls', namespace='cas_server')),@" test_venv/cas/cas/urls.py
|
||||
@ -60,3 +64,12 @@ 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
|
||||
|
||||
test_venv/bin/sphinx-build: test_venv
|
||||
test_venv/bin/pip install Sphinx sphinx_rtd_theme
|
||||
|
||||
docs: test_venv/bin/sphinx-build
|
||||
bash -c "source test_venv/bin/activate; cd docs; make html"
|
||||
|
||||
publish_pypi_release:
|
||||
python setup.py sdist bdist_wheel upload --sign
|
||||
|
55
README.rst
55
README.rst
@ -1,20 +1,7 @@
|
||||
CAS Server
|
||||
##########
|
||||
|
||||
.. image:: https://travis-ci.org/nitmir/django-cas-server.svg?branch=master
|
||||
:target: https://travis-ci.org/nitmir/django-cas-server
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/django-cas-server.svg
|
||||
:target: https://pypi.python.org/pypi/django-cas-server
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/django-cas-server.svg
|
||||
:target: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
.. image:: https://api.codacy.com/project/badge/Grade/255c21623d6946ef8802fa7995b61366
|
||||
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
|
||||
|
||||
.. image:: https://api.codacy.com/project/badge/Coverage/255c21623d6946ef8802fa7995b61366
|
||||
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
|
||||
|travis| |version| |lisence| |codacy| |coverage|
|
||||
|
||||
CAS Server is a Django application implementing the `CAS Protocol 3.0 Specification
|
||||
<https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html>`_.
|
||||
@ -22,13 +9,6 @@ CAS Server is a Django application implementing the `CAS Protocol 3.0 Specificat
|
||||
By default, the authentication process use django internal users but you can easily
|
||||
use any sources (see auth classes in the auth.py file)
|
||||
|
||||
The default 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
|
||||
like the 6.2.2 version.
|
||||
|
||||
.. contents:: Table of Contents
|
||||
|
||||
Features
|
||||
@ -52,8 +32,6 @@ Dependencies
|
||||
* Django >= 1.7 < 1.10
|
||||
* requests >= 2.4
|
||||
* requests_futures >= 0.9.5
|
||||
* django-picklefield >= 0.3.1
|
||||
* django-bootstrap3 >= 5.4 (< 7.0.0 if using django 1.7)
|
||||
* lxml >= 3.4
|
||||
* six >= 1
|
||||
|
||||
@ -68,7 +46,7 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
|
||||
|
||||
On debian like systems::
|
||||
|
||||
$ sudo apt-get install python-django python-requests python-django-picklefield python-six python-lxml
|
||||
$ sudo apt-get install python-django python-requests python-six python-lxml python-requests-futures
|
||||
|
||||
On debian jessie, you can use the version of python-django available in the
|
||||
`backports <https://backports.debian.org/Instructions/>`_.
|
||||
@ -118,7 +96,6 @@ Quick start
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
...
|
||||
'bootstrap3',
|
||||
'cas_server',
|
||||
)
|
||||
|
||||
@ -186,6 +163,17 @@ Template settings
|
||||
|
||||
* ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default
|
||||
templates. Set it to ``False`` to disable it.
|
||||
* ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary
|
||||
and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
|
||||
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
|
||||
|
||||
{
|
||||
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
|
||||
"bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js",
|
||||
"html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js",
|
||||
"respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js",
|
||||
"jquery": "//code.jquery.com/jquery.min.js",
|
||||
}
|
||||
|
||||
* ``CAS_LOGIN_TEMPLATE``: Path to the template showed on ``/login`` then the user
|
||||
is not autenticated. The default is ``"cas_server/login.html"``.
|
||||
@ -489,3 +477,20 @@ You could for example do as bellow :
|
||||
.. code-block::
|
||||
|
||||
10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate
|
||||
|
||||
|
||||
|
||||
.. |travis| image:: https://badges.genua.fr/travis/nitmir/django-cas-server/master.svg
|
||||
:target: https://travis-ci.org/nitmir/django-cas-server
|
||||
|
||||
.. |version| image:: https://badges.genua.fr/pypi/v/django-cas-server.svg
|
||||
:target: https://pypi.python.org/pypi/django-cas-server
|
||||
|
||||
.. |lisence| image:: https://badges.genua.fr/pypi/l/django-cas-server.svg
|
||||
:target: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
.. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg
|
||||
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
|
||||
|
||||
.. |coverage| image:: https://badges.genua.fr/codacy/coverage/255c21623d6946ef8802fa7995b61366/master.svg
|
||||
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
|
||||
|
@ -9,4 +9,5 @@
|
||||
#
|
||||
# (c) 2015-2016 Valentin Samir
|
||||
"""A django CAS server application"""
|
||||
#: path the the application configuration class
|
||||
default_app_config = 'cas_server.apps.CasAppConfig'
|
||||
|
@ -15,86 +15,155 @@ from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterA
|
||||
from .models import FederatedIendityProvider
|
||||
from .forms import TicketForm
|
||||
|
||||
TICKETS_READONLY_FIELDS = ('validate', 'service', 'service_pattern',
|
||||
'creation', 'renew', 'single_log_out', 'value')
|
||||
TICKETS_FIELDS = ('validate', 'service', 'service_pattern',
|
||||
'creation', 'renew', 'single_log_out')
|
||||
|
||||
class BaseInlines(admin.TabularInline):
|
||||
"""
|
||||
Bases: :class:`django.contrib.admin.TabularInline`
|
||||
|
||||
Base class for inlines in the admin interface.
|
||||
"""
|
||||
#: This controls the number of extra forms the formset will display in addition to
|
||||
#: the initial forms.
|
||||
extra = 0
|
||||
|
||||
|
||||
class ServiceTicketInline(admin.TabularInline):
|
||||
"""`ServiceTicket` in admin interface"""
|
||||
class UserAdminInlines(BaseInlines):
|
||||
"""
|
||||
Bases: :class:`BaseInlines`
|
||||
|
||||
Base class for inlines in :class:`UserAdmin` interface
|
||||
"""
|
||||
#: The form :class:`TicketForm<cas_server.forms.TicketForm>` used to display tickets.
|
||||
form = TicketForm
|
||||
#: Fields to display on a object that are read only (not editable).
|
||||
readonly_fields = (
|
||||
'validate', 'service', 'service_pattern',
|
||||
'creation', 'renew', 'single_log_out', 'value'
|
||||
)
|
||||
#: Fields to display on a object.
|
||||
fields = (
|
||||
'validate', 'service', 'service_pattern',
|
||||
'creation', 'renew', 'single_log_out'
|
||||
)
|
||||
|
||||
|
||||
class ServiceTicketInline(UserAdminInlines):
|
||||
"""
|
||||
Bases: :class:`UserAdminInlines`
|
||||
|
||||
:class:`ServiceTicket<cas_server.models.ServiceTicket>` in admin interface
|
||||
"""
|
||||
#: The model which the inline is using.
|
||||
model = ServiceTicket
|
||||
extra = 0
|
||||
form = TicketForm
|
||||
readonly_fields = TICKETS_READONLY_FIELDS
|
||||
fields = TICKETS_FIELDS
|
||||
|
||||
|
||||
class ProxyTicketInline(admin.TabularInline):
|
||||
"""`ProxyTicket` in admin interface"""
|
||||
class ProxyTicketInline(UserAdminInlines):
|
||||
"""
|
||||
Bases: :class:`UserAdminInlines`
|
||||
|
||||
:class:`ProxyTicket<cas_server.models.ProxyTicket>` in admin interface
|
||||
"""
|
||||
#: The model which the inline is using.
|
||||
model = ProxyTicket
|
||||
extra = 0
|
||||
form = TicketForm
|
||||
readonly_fields = TICKETS_READONLY_FIELDS
|
||||
fields = TICKETS_FIELDS
|
||||
|
||||
|
||||
class ProxyGrantingInline(admin.TabularInline):
|
||||
"""`ProxyGrantingTicket` in admin interface"""
|
||||
class ProxyGrantingInline(UserAdminInlines):
|
||||
"""
|
||||
Bases: :class:`UserAdminInlines`
|
||||
|
||||
:class:`ProxyGrantingTicket<cas_server.models.ProxyGrantingTicket>` in admin interface
|
||||
"""
|
||||
#: The model which the inline is using.
|
||||
model = ProxyGrantingTicket
|
||||
extra = 0
|
||||
form = TicketForm
|
||||
readonly_fields = TICKETS_READONLY_FIELDS
|
||||
fields = TICKETS_FIELDS[1:]
|
||||
|
||||
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
"""`User` in admin interface"""
|
||||
"""
|
||||
Bases: :class:`django.contrib.admin.ModelAdmin`
|
||||
|
||||
:class:`User<cas_server.models.User>` in admin interface
|
||||
"""
|
||||
#: See :class:`ServiceTicketInline`, :class:`ProxyTicketInline`, :class:`ProxyGrantingInline`
|
||||
#: objects below the :class:`UserAdmin` fields.
|
||||
inlines = (ServiceTicketInline, ProxyTicketInline, ProxyGrantingInline)
|
||||
#: Fields to display on a object that are read only (not editable).
|
||||
readonly_fields = ('username', 'date', "session_key")
|
||||
#: Fields to display on a object.
|
||||
fields = ('username', 'date', "session_key")
|
||||
#: Fields to display on the list of class:`UserAdmin` objects.
|
||||
list_display = ('username', 'date', "session_key")
|
||||
|
||||
|
||||
class UsernamesInline(admin.TabularInline):
|
||||
"""`Username` in admin interface"""
|
||||
class UsernamesInline(BaseInlines):
|
||||
"""
|
||||
Bases: :class:`BaseInlines`
|
||||
|
||||
:class:`Username<cas_server.models.Username>` in admin interface
|
||||
"""
|
||||
#: The model which the inline is using.
|
||||
model = Username
|
||||
extra = 0
|
||||
|
||||
|
||||
class ReplaceAttributNameInline(admin.TabularInline):
|
||||
"""`ReplaceAttributName` in admin interface"""
|
||||
class ReplaceAttributNameInline(BaseInlines):
|
||||
"""
|
||||
Bases: :class:`BaseInlines`
|
||||
|
||||
:class:`ReplaceAttributName<cas_server.models.ReplaceAttributName>` in admin interface
|
||||
"""
|
||||
#: The model which the inline is using.
|
||||
model = ReplaceAttributName
|
||||
extra = 0
|
||||
|
||||
|
||||
class ReplaceAttributValueInline(admin.TabularInline):
|
||||
"""`ReplaceAttributValue` in admin interface"""
|
||||
class ReplaceAttributValueInline(BaseInlines):
|
||||
"""
|
||||
Bases: :class:`BaseInlines`
|
||||
|
||||
:class:`ReplaceAttributValue<cas_server.models.ReplaceAttributValue>` in admin interface
|
||||
"""
|
||||
#: The model which the inline is using.
|
||||
model = ReplaceAttributValue
|
||||
extra = 0
|
||||
|
||||
|
||||
class FilterAttributValueInline(admin.TabularInline):
|
||||
"""`FilterAttributValue` in admin interface"""
|
||||
class FilterAttributValueInline(BaseInlines):
|
||||
"""
|
||||
Bases: :class:`BaseInlines`
|
||||
|
||||
:class:`FilterAttributValue<cas_server.models.FilterAttributValue>` in admin interface
|
||||
"""
|
||||
#: The model which the inline is using.
|
||||
model = FilterAttributValue
|
||||
extra = 0
|
||||
|
||||
|
||||
class ServicePatternAdmin(admin.ModelAdmin):
|
||||
"""`ServicePattern` in admin interface"""
|
||||
"""
|
||||
Bases: :class:`django.contrib.admin.ModelAdmin`
|
||||
|
||||
:class:`ServicePattern<cas_server.models.ServicePattern>` in admin interface
|
||||
"""
|
||||
#: See :class:`UsernamesInline`, :class:`ReplaceAttributNameInline`,
|
||||
#: :class:`ReplaceAttributValueInline`, :class:`FilterAttributValueInline` objects below
|
||||
#: the :class:`ServicePatternAdmin` fields.
|
||||
inlines = (
|
||||
UsernamesInline,
|
||||
ReplaceAttributNameInline,
|
||||
ReplaceAttributValueInline,
|
||||
FilterAttributValueInline
|
||||
)
|
||||
#: Fields to display on the list of class:`ServicePatternAdmin` objects.
|
||||
list_display = ('pos', 'name', 'pattern', 'proxy',
|
||||
'single_log_out', 'proxy_callback', 'restrict_users')
|
||||
|
||||
|
||||
class FederatedIendityProviderAdmin(admin.ModelAdmin):
|
||||
"""`FederatedIendityProvider` in admin interface"""
|
||||
"""
|
||||
Bases: :class:`django.contrib.admin.ModelAdmin`
|
||||
|
||||
:class:`FederatedIendityProvider<cas_server.models.FederatedIendityProvider>` in admin
|
||||
interface
|
||||
"""
|
||||
#: Fields to display on a object.
|
||||
fields = ('pos', 'suffix', 'server_url', 'cas_protocol_version', 'verbose_name', 'display')
|
||||
#: Fields to display on the list of class:`FederatedIendityProviderAdmin` objects.
|
||||
list_display = ('verbose_name', 'suffix', 'display')
|
||||
|
||||
|
||||
|
@ -14,6 +14,12 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class CasAppConfig(AppConfig):
|
||||
"""django CAS application config class"""
|
||||
"""
|
||||
Bases: :class:`django.apps.AppConfig`
|
||||
|
||||
django CAS application config class
|
||||
"""
|
||||
#: Full Python path to the application. It must be unique across a Django project.
|
||||
name = 'cas_server'
|
||||
#: Human-readable name for the application.
|
||||
verbose_name = _('Central Authentication Service')
|
||||
|
@ -26,55 +26,112 @@ from .models import FederatedUser
|
||||
|
||||
|
||||
class AuthUser(object):
|
||||
"""Authentication base class"""
|
||||
"""
|
||||
Authentication base class
|
||||
|
||||
:param unicode username: A username, stored in the :attr:`username` class attribute.
|
||||
"""
|
||||
|
||||
#: username used to instanciate the current object
|
||||
username = None
|
||||
|
||||
def __init__(self, username):
|
||||
self.username = username
|
||||
|
||||
def test_password(self, password):
|
||||
"""test `password` agains the user"""
|
||||
"""
|
||||
Tests ``password`` agains the user password.
|
||||
|
||||
:raises NotImplementedError: always. The method need to be implemented by subclasses
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def attributs(self):
|
||||
"""return a dict of user attributes"""
|
||||
"""
|
||||
The user attributes.
|
||||
|
||||
raises NotImplementedError: always. The method need to be implemented by subclasses
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class DummyAuthUser(AuthUser): # pragma: no cover
|
||||
"""A Dummy authentication class"""
|
||||
"""
|
||||
A Dummy authentication class. Authentication always fails
|
||||
|
||||
def __init__(self, username):
|
||||
super(DummyAuthUser, self).__init__(username)
|
||||
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||
class attribute. There is no valid value for this attribute here.
|
||||
"""
|
||||
|
||||
def test_password(self, password):
|
||||
"""test `password` agains the user"""
|
||||
"""
|
||||
Tests ``password`` agains the user password.
|
||||
|
||||
:param unicode password: a clear text password as submited by the user.
|
||||
:return: always ``False``
|
||||
:rtype: bool
|
||||
"""
|
||||
return False
|
||||
|
||||
def attributs(self):
|
||||
"""return a dict of user attributes"""
|
||||
"""
|
||||
The user attributes.
|
||||
|
||||
:return: en empty :class:`dict`.
|
||||
:rtype: dict
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
class TestAuthUser(AuthUser):
|
||||
"""A test authentication class with one user test having
|
||||
alose test as password and some attributes"""
|
||||
"""
|
||||
A test authentication class only working for one unique user.
|
||||
|
||||
def __init__(self, username):
|
||||
super(TestAuthUser, self).__init__(username)
|
||||
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||
class attribute. The uniq valid value is ``settings.CAS_TEST_USER``.
|
||||
"""
|
||||
|
||||
def test_password(self, password):
|
||||
"""test `password` agains the user"""
|
||||
"""
|
||||
Tests ``password`` agains the user password.
|
||||
|
||||
:param unicode password: a clear text password as submited by the user.
|
||||
:return: ``True`` if :attr:`username<AuthUser.username>` is valid and
|
||||
``password`` is equal to ``settings.CAS_TEST_PASSWORD``, ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
return self.username == settings.CAS_TEST_USER and password == settings.CAS_TEST_PASSWORD
|
||||
|
||||
def attributs(self):
|
||||
"""return a dict of user attributes"""
|
||||
"""
|
||||
The user attributes.
|
||||
|
||||
:return: the ``settings.CAS_TEST_ATTRIBUTES`` :class:`dict` if
|
||||
:attr:`username<AuthUser.username>` is valid, an empty :class:`dict` otherwise.
|
||||
:rtype: dict
|
||||
"""
|
||||
if self.username == settings.CAS_TEST_USER:
|
||||
return settings.CAS_TEST_ATTRIBUTES
|
||||
else: # pragma: no cover (should not happen)
|
||||
return {}
|
||||
|
||||
|
||||
class MysqlAuthUser(AuthUser): # pragma: no cover
|
||||
"""A mysql auth class: authentication user agains a mysql database"""
|
||||
"""
|
||||
A mysql authentication class: authentication user agains a mysql database
|
||||
|
||||
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||
class attribute. Valid value are fetched from the MySQL database set with
|
||||
``settings.CAS_SQL_*`` settings parameters using the query
|
||||
``settings.CAS_SQL_USER_QUERY``.
|
||||
"""
|
||||
#: Mysql user attributes as a :class:`dict` if the username is found in the database.
|
||||
user = None
|
||||
|
||||
def __init__(self, username):
|
||||
# see the connect function at
|
||||
# http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes
|
||||
# for possible mysql config parameters.
|
||||
mysql_config = {
|
||||
"user": settings.CAS_SQL_USERNAME,
|
||||
"passwd": settings.CAS_SQL_PASSWORD,
|
||||
@ -94,7 +151,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
|
||||
super(MysqlAuthUser, self).__init__(username)
|
||||
|
||||
def test_password(self, password):
|
||||
"""test `password` agains the user"""
|
||||
"""
|
||||
Tests ``password`` agains the user password.
|
||||
|
||||
:param unicode password: a clear text password as submited by the user.
|
||||
:return: ``True`` if :attr:`username<AuthUser.username>` is valid and ``password`` is
|
||||
correct, ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
if self.user:
|
||||
return check_password(
|
||||
settings.CAS_SQL_PASSWORD_CHECK,
|
||||
@ -106,7 +170,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
|
||||
return False
|
||||
|
||||
def attributs(self):
|
||||
"""return a dict of user attributes"""
|
||||
"""
|
||||
The user attributes.
|
||||
|
||||
:return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode`
|
||||
or :class:`list` of :func:`unicode`. If the user do not exists, the returned
|
||||
:class:`dict` is empty.
|
||||
:rtype: dict
|
||||
"""
|
||||
if self.user:
|
||||
return self.user
|
||||
else:
|
||||
@ -114,7 +185,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
|
||||
|
||||
|
||||
class DjangoAuthUser(AuthUser): # pragma: no cover
|
||||
"""A django auth class: authenticate user agains django internal users"""
|
||||
"""
|
||||
A django auth class: authenticate user agains django internal users
|
||||
|
||||
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||
class attribute. Valid value are usernames of django internal users.
|
||||
"""
|
||||
#: a django user object if the username is found. The user model is retreived
|
||||
#: using :func:`django.contrib.auth.get_user_model`.
|
||||
user = None
|
||||
|
||||
def __init__(self, username):
|
||||
@ -126,14 +204,27 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
|
||||
super(DjangoAuthUser, self).__init__(username)
|
||||
|
||||
def test_password(self, password):
|
||||
"""test `password` agains the user"""
|
||||
"""
|
||||
Tests ``password`` agains the user password.
|
||||
|
||||
:param unicode password: a clear text password as submited by the user.
|
||||
:return: ``True`` if :attr:`user` is valid and ``password`` is
|
||||
correct, ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
if self.user:
|
||||
return self.user.check_password(password)
|
||||
else:
|
||||
return False
|
||||
|
||||
def attributs(self):
|
||||
"""return a dict of user attributes"""
|
||||
"""
|
||||
The user attributes, defined as the fields on the :attr:`user` object.
|
||||
|
||||
:return: a :class:`dict` with the :attr:`user` object fields. Attributes may be
|
||||
If the user do not exists, the returned :class:`dict` is empty.
|
||||
:rtype: dict
|
||||
"""
|
||||
if self.user:
|
||||
attr = {}
|
||||
for field in self.user._meta.fields:
|
||||
@ -144,7 +235,16 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
|
||||
|
||||
|
||||
class CASFederateAuth(AuthUser):
|
||||
"""Authentication class used then CAS_FEDERATE is True"""
|
||||
"""
|
||||
Authentication class used then CAS_FEDERATE is True
|
||||
|
||||
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||
class attribute. Valid value are usernames of
|
||||
:class:`FederatedUser<cas_server.models.FederatedUser>` object.
|
||||
:class:`FederatedUser<cas_server.models.FederatedUser>` object are created on CAS
|
||||
backends successful ticket validation.
|
||||
"""
|
||||
#: a :class`FederatedUser<cas_server.models.FederatedUser>` object if ``username`` is found.
|
||||
user = None
|
||||
|
||||
def __init__(self, username):
|
||||
@ -157,7 +257,17 @@ class CASFederateAuth(AuthUser):
|
||||
super(CASFederateAuth, self).__init__(username)
|
||||
|
||||
def test_password(self, ticket):
|
||||
"""test `password` agains the user"""
|
||||
"""
|
||||
Tests ``password`` agains the user password.
|
||||
|
||||
:param unicode password: The CAS tickets just used to validate the user authentication
|
||||
against its CAS backend.
|
||||
:return: ``True`` if :attr:`user` is valid and ``password`` is
|
||||
a ticket validated less than ``settings.CAS_TICKET_VALIDITY`` secondes and has not
|
||||
being previously used for authenticated this
|
||||
:class:`FederatedUser<cas_server.models.FederatedUser>`. ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not self.user or not self.user.ticket:
|
||||
return False
|
||||
else:
|
||||
@ -168,7 +278,13 @@ class CASFederateAuth(AuthUser):
|
||||
)
|
||||
|
||||
def attributs(self):
|
||||
"""return a dict of user attributes"""
|
||||
"""
|
||||
The user attributes, as returned by the CAS backend.
|
||||
|
||||
:return: :obj:`FederatedUser.attributs<cas_server.models.FederatedUser.attributs>`.
|
||||
If the user do not exists, the returned :class:`dict` is empty.
|
||||
:rtype: dict
|
||||
"""
|
||||
if not self.user: # pragma: no cover (should not happen)
|
||||
return {}
|
||||
else:
|
||||
|
@ -36,7 +36,7 @@ class CASError(ValueError):
|
||||
|
||||
class ReturnUnicode(object):
|
||||
@staticmethod
|
||||
def unicode(string, charset):
|
||||
def u(string, charset):
|
||||
if not isinstance(string, six.text_type):
|
||||
return string.decode(charset)
|
||||
else:
|
||||
@ -157,7 +157,7 @@ class CASClientV1(CASClientBase, ReturnUnicode):
|
||||
charset = content_type.split("charset=")[-1]
|
||||
else:
|
||||
charset = "ascii"
|
||||
user = self.unicode(page.readline().strip(), charset)
|
||||
user = self.u(page.readline().strip(), charset)
|
||||
return user, None, None
|
||||
else:
|
||||
return None, None, None
|
||||
@ -202,18 +202,18 @@ class CASClientV2(CASClientBase, ReturnUnicode):
|
||||
def parse_attributes_xml_element(cls, element, charset):
|
||||
attributes = dict()
|
||||
for attribute in element:
|
||||
tag = cls.self.unicode(attribute.tag, charset).split(u"}").pop()
|
||||
tag = cls.self.u(attribute.tag, charset).split(u"}").pop()
|
||||
if tag in attributes:
|
||||
if isinstance(attributes[tag], list):
|
||||
attributes[tag].append(cls.unicode(attribute.text, charset))
|
||||
attributes[tag].append(cls.u(attribute.text, charset))
|
||||
else:
|
||||
attributes[tag] = [attributes[tag]]
|
||||
attributes[tag].append(cls.unicode(attribute.text, charset))
|
||||
attributes[tag].append(cls.u(attribute.text, charset))
|
||||
else:
|
||||
if tag == u'attraStyle':
|
||||
pass
|
||||
else:
|
||||
attributes[tag] = cls.unicode(attribute.text, charset)
|
||||
attributes[tag] = cls.u(attribute.text, charset)
|
||||
return attributes
|
||||
|
||||
@classmethod
|
||||
@ -238,9 +238,9 @@ class CASClientV2(CASClientBase, ReturnUnicode):
|
||||
if tree[0].tag.endswith('authenticationSuccess'):
|
||||
for element in tree[0]:
|
||||
if element.tag.endswith('user'):
|
||||
user = cls.unicode(element.text, charset)
|
||||
user = cls.u(element.text, charset)
|
||||
elif element.tag.endswith('proxyGrantingTicket'):
|
||||
pgtiou = cls.unicode(element.text, charset)
|
||||
pgtiou = cls.u(element.text, charset)
|
||||
elif element.tag.endswith('attributes'):
|
||||
attributes = cls.parse_attributes_xml_element(element, charset)
|
||||
return user, attributes, pgtiou
|
||||
@ -255,15 +255,15 @@ class CASClientV3(CASClientV2, SingleLogoutMixin):
|
||||
def parse_attributes_xml_element(cls, element, charset):
|
||||
attributes = dict()
|
||||
for attribute in element:
|
||||
tag = cls.unicode(attribute.tag, charset).split(u"}").pop()
|
||||
tag = cls.u(attribute.tag, charset).split(u"}").pop()
|
||||
if tag in attributes:
|
||||
if isinstance(attributes[tag], list):
|
||||
attributes[tag].append(cls.unicode(attribute.text, charset))
|
||||
attributes[tag].append(cls.u(attribute.text, charset))
|
||||
else:
|
||||
attributes[tag] = [attributes[tag]]
|
||||
attributes[tag].append(cls.unicode(attribute.text, charset))
|
||||
attributes[tag].append(cls.u(attribute.text, charset))
|
||||
else:
|
||||
attributes[tag] = cls.unicode(attribute.text, charset)
|
||||
attributes[tag] = cls.u(attribute.text, charset)
|
||||
return attributes
|
||||
|
||||
@classmethod
|
||||
@ -323,25 +323,25 @@ class CASClientWithSAMLV1(CASClientV2, SingleLogoutMixin):
|
||||
# User is validated
|
||||
name_identifier = tree.find('.//' + SAML_1_0_ASSERTION_NS + 'NameIdentifier')
|
||||
if name_identifier is not None:
|
||||
user = self.unicode(name_identifier.text, charset)
|
||||
user = self.u(name_identifier.text, charset)
|
||||
attrs = tree.findall('.//' + SAML_1_0_ASSERTION_NS + 'Attribute')
|
||||
for at in attrs:
|
||||
if self.username_attribute in list(at.attrib.values()):
|
||||
user = self.unicode(
|
||||
user = self.u(
|
||||
at.find(SAML_1_0_ASSERTION_NS + 'AttributeValue').text,
|
||||
charset
|
||||
)
|
||||
attributes[u'uid'] = user
|
||||
|
||||
values = at.findall(SAML_1_0_ASSERTION_NS + 'AttributeValue')
|
||||
key = self.unicode(at.attrib['AttributeName'], charset)
|
||||
key = self.u(at.attrib['AttributeName'], charset)
|
||||
if len(values) > 1:
|
||||
values_array = []
|
||||
for v in values:
|
||||
values_array.append(self.unicode(v.text, charset))
|
||||
values_array.append(self.u(v.text, charset))
|
||||
attributes[key] = values_array
|
||||
else:
|
||||
attributes[key] = self.unicode(values[0].text, charset)
|
||||
attributes[key] = self.u(values[0].text, charset)
|
||||
return user, attributes, None
|
||||
finally:
|
||||
page.close()
|
||||
|
@ -13,84 +13,146 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
def setting_default(name, default_value):
|
||||
"""if the config `name` is not set, set it the `default_value`"""
|
||||
value = getattr(settings, name, default_value)
|
||||
setattr(settings, name, value)
|
||||
|
||||
setting_default('CAS_LOGO_URL', static("cas_server/logo.png"))
|
||||
#: URL to the logo showed in the up left corner on the default templates.
|
||||
CAS_LOGO_URL = static("cas_server/logo.png")
|
||||
#: URLs to css and javascript external components.
|
||||
CAS_COMPONENT_URLS = {
|
||||
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
|
||||
"bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js",
|
||||
"html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js",
|
||||
"respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js",
|
||||
"jquery": "//code.jquery.com/jquery.min.js",
|
||||
}
|
||||
#: Path to the template showed on /login then the user is not autenticated.
|
||||
CAS_LOGIN_TEMPLATE = 'cas_server/login.html'
|
||||
#: Path to the template showed on /login?service=... then the user is authenticated and has asked
|
||||
#: to be warned before being connected to a service.
|
||||
CAS_WARN_TEMPLATE = 'cas_server/warn.html'
|
||||
#: Path to the template showed on /login then to user is authenticated.
|
||||
CAS_LOGGED_TEMPLATE = 'cas_server/logged.html'
|
||||
#: Path to the template showed on /logout then to user is being disconnected.
|
||||
CAS_LOGOUT_TEMPLATE = 'cas_server/logout.html'
|
||||
#: Should we redirect users to /login after they logged out instead of displaying
|
||||
#: :obj:`CAS_LOGOUT_TEMPLATE`.
|
||||
CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False
|
||||
|
||||
setting_default('CAS_LOGIN_TEMPLATE', 'cas_server/login.html')
|
||||
setting_default('CAS_WARN_TEMPLATE', 'cas_server/warn.html')
|
||||
setting_default('CAS_LOGGED_TEMPLATE', 'cas_server/logged.html')
|
||||
setting_default('CAS_LOGOUT_TEMPLATE', 'cas_server/logout.html')
|
||||
setting_default('CAS_AUTH_CLASS', 'cas_server.auth.DjangoAuthUser')
|
||||
# All CAS implementation MUST support ST and PT up to 32 chars,
|
||||
# PGT and PGTIOU up to 64 chars and it is RECOMMENDED that all
|
||||
# tickets up to 256 chars are supports so we use 64 for the default
|
||||
# len.
|
||||
setting_default('CAS_TICKET_LEN', 64)
|
||||
|
||||
setting_default('CAS_LT_LEN', settings.CAS_TICKET_LEN)
|
||||
setting_default('CAS_ST_LEN', settings.CAS_TICKET_LEN)
|
||||
setting_default('CAS_PT_LEN', settings.CAS_TICKET_LEN)
|
||||
setting_default('CAS_PGT_LEN', settings.CAS_TICKET_LEN)
|
||||
setting_default('CAS_PGTIOU_LEN', settings.CAS_TICKET_LEN)
|
||||
#: A dotted path to a class or a class implementing cas_server.auth.AuthUser.
|
||||
CAS_AUTH_CLASS = 'cas_server.auth.DjangoAuthUser'
|
||||
#: Path to certificate authorities file. Usually on linux the local CAs are in
|
||||
#: /etc/ssl/certs/ca-certificates.crt. ``True`` tell requests to use its internal certificat
|
||||
#: authorities.
|
||||
CAS_PROXY_CA_CERTIFICATE_PATH = True
|
||||
#: Maximum number of parallel single log out requests send
|
||||
#: if more requests need to be send, there are queued
|
||||
CAS_SLO_MAX_PARALLEL_REQUESTS = 10
|
||||
#: Timeout for a single SLO request in seconds.
|
||||
CAS_SLO_TIMEOUT = 5
|
||||
#: Shared to transmit then using the view :class:`cas_server.views.Auth`
|
||||
CAS_AUTH_SHARED_SECRET = ''
|
||||
|
||||
setting_default('CAS_TICKET_VALIDITY', 60)
|
||||
setting_default('CAS_PGT_VALIDITY', 3600)
|
||||
setting_default('CAS_TICKET_TIMEOUT', 24*3600)
|
||||
setting_default('CAS_PROXY_CA_CERTIFICATE_PATH', True)
|
||||
setting_default('CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT', False)
|
||||
|
||||
setting_default('CAS_AUTH_SHARED_SECRET', '')
|
||||
#: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time
|
||||
#: between ticket issuance by the CAS and ticket validation by an application.
|
||||
CAS_TICKET_VALIDITY = 60
|
||||
#: Number of seconds the proxy granting tickets are valid.
|
||||
CAS_PGT_VALIDITY = 3600
|
||||
#: Number of seconds a ticket is kept in the database before sending Single Log Out request and
|
||||
#: being cleared.
|
||||
CAS_TICKET_TIMEOUT = 24*3600
|
||||
|
||||
setting_default('CAS_LOGIN_TICKET_PREFIX', 'LT')
|
||||
# Service tickets MUST begin with the characters ST so you should not change this
|
||||
# Services MUST be able to accept service tickets of up to 32 characters in length
|
||||
setting_default('CAS_SERVICE_TICKET_PREFIX', 'ST')
|
||||
# Proxy tickets SHOULD begin with the characters, PT.
|
||||
# Back-end services MUST be able to accept proxy tickets of up to 32 characters.
|
||||
setting_default('CAS_PROXY_TICKET_PREFIX', 'PT')
|
||||
# Proxy-granting tickets SHOULD begin with the characters PGT
|
||||
# Services MUST be able to handle proxy-granting tickets of up to 64
|
||||
setting_default('CAS_PROXY_GRANTING_TICKET_PREFIX', 'PGT')
|
||||
# Proxy-granting ticket IOUs SHOULD begin with the characters, PGTIOU
|
||||
# Services MUST be able to handle PGTIOUs of up to 64 characters in length.
|
||||
setting_default('CAS_PROXY_GRANTING_TICKET_IOU_PREFIX', 'PGTIOU')
|
||||
|
||||
# Maximum number of parallel single log out requests send
|
||||
# if more requests need to be send, there are queued
|
||||
setting_default('CAS_SLO_MAX_PARALLEL_REQUESTS', 10)
|
||||
# SLO request timeout.
|
||||
setting_default('CAS_SLO_TIMEOUT', 5)
|
||||
#: All CAS implementation MUST support ST and PT up to 32 chars,
|
||||
#: PGT and PGTIOU up to 64 chars and it is RECOMMENDED that all
|
||||
#: tickets up to 256 chars are supports so we use 64 for the default
|
||||
#: len.
|
||||
CAS_TICKET_LEN = 64
|
||||
|
||||
setting_default('CAS_SQL_HOST', 'localhost')
|
||||
setting_default('CAS_SQL_USERNAME', '')
|
||||
setting_default('CAS_SQL_PASSWORD', '')
|
||||
setting_default('CAS_SQL_DBNAME', '')
|
||||
setting_default('CAS_SQL_DBCHARSET', 'utf8')
|
||||
setting_default('CAS_SQL_USER_QUERY', 'SELECT user AS usersame, pass AS '
|
||||
'password, users.* FROM users WHERE user = %s')
|
||||
setting_default('CAS_SQL_PASSWORD_CHECK', 'crypt') # crypt or plain
|
||||
#: alias of :obj:`settings.CAS_TICKET_LEN`
|
||||
CAS_LT_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN)
|
||||
#: alias of :obj:`settings.CAS_TICKET_LEN`
|
||||
#: Services MUST be able to accept service tickets of up to 32 characters in length.
|
||||
CAS_ST_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN)
|
||||
#: alias of :obj:`settings.CAS_TICKET_LEN`
|
||||
#: Back-end services MUST be able to accept proxy tickets of up to 32 characters.
|
||||
CAS_PT_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN)
|
||||
#: alias of :obj:`settings.CAS_TICKET_LEN`
|
||||
#: Services MUST be able to handle proxy-granting tickets of up to 64
|
||||
CAS_PGT_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN)
|
||||
#: alias of :obj:`settings.CAS_TICKET_LEN`
|
||||
#: Services MUST be able to handle PGTIOUs of up to 64 characters in length.
|
||||
CAS_PGTIOU_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN)
|
||||
|
||||
setting_default('CAS_TEST_USER', 'test')
|
||||
setting_default('CAS_TEST_PASSWORD', 'test')
|
||||
setting_default(
|
||||
'CAS_TEST_ATTRIBUTES',
|
||||
{
|
||||
#: Prefix of login tickets.
|
||||
CAS_LOGIN_TICKET_PREFIX = u'LT'
|
||||
#: Prefix of service tickets. Service tickets MUST begin with the characters ST so you should not
|
||||
#: change this.
|
||||
CAS_SERVICE_TICKET_PREFIX = u'ST'
|
||||
#: Prefix of proxy ticket. Proxy tickets SHOULD begin with the characters, PT.
|
||||
CAS_PROXY_TICKET_PREFIX = u'PT'
|
||||
#: Prefix of proxy granting ticket. Proxy-granting tickets SHOULD begin with the characters PGT.
|
||||
CAS_PROXY_GRANTING_TICKET_PREFIX = u'PGT'
|
||||
#: Prefix of proxy granting ticket IOU. Proxy-granting ticket IOUs SHOULD begin with the characters
|
||||
#: PGTIOU.
|
||||
CAS_PROXY_GRANTING_TICKET_IOU_PREFIX = u'PGTIOU'
|
||||
|
||||
|
||||
#: Host for the SQL server.
|
||||
CAS_SQL_HOST = 'localhost'
|
||||
#: Username for connecting to the SQL server.
|
||||
CAS_SQL_USERNAME = ''
|
||||
#: Password for connecting to the SQL server.
|
||||
CAS_SQL_PASSWORD = ''
|
||||
#: Database name.
|
||||
CAS_SQL_DBNAME = ''
|
||||
#: Database charset.
|
||||
CAS_SQL_DBCHARSET = 'utf8'
|
||||
#: The query performed upon user authentication.
|
||||
CAS_SQL_USER_QUERY = 'SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s'
|
||||
#: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``,
|
||||
#: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``,
|
||||
#: ``"hex_sha512"``, ``"plain"``.
|
||||
CAS_SQL_PASSWORD_CHECK = 'crypt' # crypt or plain
|
||||
|
||||
|
||||
#: Username of the test user.
|
||||
CAS_TEST_USER = 'test'
|
||||
#: Password of the test user.
|
||||
CAS_TEST_PASSWORD = 'test'
|
||||
#: Attributes of the test user.
|
||||
CAS_TEST_ATTRIBUTES = {
|
||||
'nom': 'Nymous',
|
||||
'prenom': 'Ano',
|
||||
'email': 'anonymous@example.net',
|
||||
'alias': ['demo1', 'demo2']
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setting_default('CAS_ENABLE_AJAX_AUTH', False)
|
||||
|
||||
setting_default('CAS_FEDERATE', False)
|
||||
setting_default('CAS_FEDERATE_REMEMBER_TIMEOUT', 604800) # one week
|
||||
#: A :class:`bool` for activatinc the hability to fetch tickets using javascript.
|
||||
CAS_ENABLE_AJAX_AUTH = False
|
||||
|
||||
|
||||
#: A :class:`bool` for activating the federated mode
|
||||
CAS_FEDERATE = False
|
||||
#: Time after witch the cookie use for “remember my identity provider” expire (one week).
|
||||
CAS_FEDERATE_REMEMBER_TIMEOUT = 604800
|
||||
|
||||
GLOBALS = globals().copy()
|
||||
for name, default_value in GLOBALS.items():
|
||||
# get the current setting value, falling back to default_value
|
||||
value = getattr(settings, name, default_value)
|
||||
# set the setting value to its value if defined, ellse to the default_value.
|
||||
setattr(settings, name, value)
|
||||
|
||||
|
||||
# if the federated mode is enabled, we must use the :class`cas_server.auth.CASFederateAuth` auth
|
||||
# backend.
|
||||
if settings.CAS_FEDERATE:
|
||||
settings.CAS_AUTH_CLASS = "cas_server.auth.CASFederateAuth"
|
||||
|
||||
|
||||
#: SessionStore class depending of :django:setting:`SESSION_ENGINE`
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
@ -10,26 +10,37 @@
|
||||
#
|
||||
# (c) 2016 Valentin Samir
|
||||
"""federated mode helper classes"""
|
||||
from .default_settings import settings
|
||||
from .default_settings import SessionStore
|
||||
from django.db import IntegrityError
|
||||
|
||||
from .cas import CASClient
|
||||
from .models import FederatedUser, FederateSLO, User
|
||||
|
||||
import logging
|
||||
from importlib import import_module
|
||||
from six.moves import urllib
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
#: logger facility
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CASFederateValidateUser(object):
|
||||
"""Class CAS client used to authenticate the user again a CAS provider"""
|
||||
"""
|
||||
Class CAS client used to authenticate the user again a CAS provider
|
||||
|
||||
:param cas_server.models.FederatedIendityProvider provider: The provider to use for
|
||||
authenticate the user.
|
||||
:param unicode service_url: The service url to transmit to the ``provider``.
|
||||
"""
|
||||
#: the provider returned username
|
||||
username = None
|
||||
#: the provider returned attributes
|
||||
attributs = {}
|
||||
#: the CAS client instance
|
||||
client = None
|
||||
#: the provider returned username this the provider suffix appended
|
||||
federated_username = None
|
||||
#: the identity provider
|
||||
provider = None
|
||||
|
||||
def __init__(self, provider, service_url):
|
||||
self.provider = provider
|
||||
@ -41,15 +52,31 @@ class CASFederateValidateUser(object):
|
||||
)
|
||||
|
||||
def get_login_url(self):
|
||||
"""return the CAS provider login url"""
|
||||
"""
|
||||
:return: the CAS provider login url
|
||||
:rtype: unicode
|
||||
"""
|
||||
return self.client.get_login_url()
|
||||
|
||||
def get_logout_url(self, redirect_url=None):
|
||||
"""return the CAS provider logout url"""
|
||||
"""
|
||||
:param redirect_url: The url to redirect to after logout from the provider, if provided.
|
||||
:type redirect_url: :obj:`unicode` or :obj:`NoneType<types.NoneType>`
|
||||
:return: the CAS provider logout url
|
||||
:rtype: unicode
|
||||
"""
|
||||
return self.client.get_logout_url(redirect_url)
|
||||
|
||||
def verify_ticket(self, ticket):
|
||||
"""test `ticket` agains the CAS provider, if valid, create the local federated user"""
|
||||
"""
|
||||
test ``ticket`` agains the CAS provider, if valid, create a
|
||||
:class:`FederatedUser<cas_server.models.FederatedUser>` matching provider returned
|
||||
username and attributes.
|
||||
|
||||
:param unicode ticket: The ticket to validate against the provider CAS
|
||||
:return: ``True`` if the validation succeed, else ``False``.
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
username, attributs = self.client.verify_ticket(ticket)[:2]
|
||||
except urllib.error.URLError:
|
||||
@ -57,7 +84,7 @@ class CASFederateValidateUser(object):
|
||||
if username is not None:
|
||||
if attributs is None:
|
||||
attributs = {}
|
||||
attributs["provider"] = self.provider
|
||||
attributs["provider"] = self.provider.suffix
|
||||
self.username = username
|
||||
self.attributs = attributs
|
||||
user = FederatedUser.objects.update_or_create(
|
||||
@ -73,7 +100,15 @@ class CASFederateValidateUser(object):
|
||||
|
||||
@staticmethod
|
||||
def register_slo(username, session_key, ticket):
|
||||
"""association a ticket with a (username, session) for processing later SLO request"""
|
||||
"""
|
||||
association a ``ticket`` with a (``username``, ``session_key``) for processing later SLO
|
||||
request by creating a :class:`cas_server.models.FederateSLO` object.
|
||||
|
||||
:param unicode username: A logged user username, with the ``@`` component.
|
||||
:param unicode session_key: A logged user session_key matching ``username``.
|
||||
:param unicode ticket: A ticket used to authentication ``username`` for the session
|
||||
``session_key``.
|
||||
"""
|
||||
try:
|
||||
FederateSLO.objects.create(
|
||||
username=username,
|
||||
@ -84,7 +119,14 @@ class CASFederateValidateUser(object):
|
||||
pass
|
||||
|
||||
def clean_sessions(self, logout_request):
|
||||
"""process a SLO request"""
|
||||
"""
|
||||
process a SLO request: Search for ticket values in ``logout_request``. For each
|
||||
ticket value matching a :class:`cas_server.models.FederateSLO`, disconnect the
|
||||
corresponding user.
|
||||
|
||||
:param unicode logout_request: An XML document contening one or more Single Log Out
|
||||
requests.
|
||||
"""
|
||||
try:
|
||||
slos = self.client.get_saml_slos(logout_request) or []
|
||||
except NameError: # pragma: no cover (should not happen)
|
||||
|
@ -18,21 +18,55 @@ import cas_server.utils as utils
|
||||
import cas_server.models as models
|
||||
|
||||
|
||||
class WarnForm(forms.Form):
|
||||
"""Form used on warn page before emiting a ticket"""
|
||||
class BootsrapForm(forms.Form):
|
||||
"""Form base class to use boostrap then rendering the form fields"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BootsrapForm, self).__init__(*args, **kwargs)
|
||||
for (name, field) in self.fields.items():
|
||||
# Only tweak the fiel if it will be displayed
|
||||
if not isinstance(field.widget, forms.HiddenInput):
|
||||
# tell to display the field (used in form.html)
|
||||
self[name].display = True
|
||||
attrs = {}
|
||||
if isinstance(field.widget, forms.CheckboxInput):
|
||||
self[name].checkbox = True
|
||||
else:
|
||||
attrs['class'] = "form-control"
|
||||
if field.label:
|
||||
attrs["placeholder"] = field.label
|
||||
if field.required:
|
||||
attrs["required"] = "required"
|
||||
field.widget.attrs.update(attrs)
|
||||
|
||||
|
||||
class WarnForm(BootsrapForm):
|
||||
"""
|
||||
Bases: :class:`django.forms.Form`
|
||||
|
||||
Form used on warn page before emiting a ticket
|
||||
"""
|
||||
|
||||
#: The service url for which the user want a ticket
|
||||
service = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: Is the service asking the authentication renewal ?
|
||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
#: Url to redirect to if the authentication fail (user not authenticated or bad service)
|
||||
gateway = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: ``True`` if the user has been warned of the ticket emission
|
||||
warned = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
#: A valid LoginTicket to prevent POST replay
|
||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
|
||||
class FederateSelect(forms.Form):
|
||||
class FederateSelect(BootsrapForm):
|
||||
"""
|
||||
Form used on the login page when CAS_FEDERATE is True
|
||||
allowing the user to choose a identity provider.
|
||||
Bases: :class:`django.forms.Form`
|
||||
|
||||
Form used on the login page when ``settings.CAS_FEDERATE`` is ``True``
|
||||
allowing the user to choose an identity provider.
|
||||
"""
|
||||
#: The providers the user can choose to be used as authentication backend
|
||||
provider = forms.ModelChoiceField(
|
||||
queryset=models.FederatedIendityProvider.objects.filter(display=True).order_by(
|
||||
"pos",
|
||||
@ -42,27 +76,49 @@ class FederateSelect(forms.Form):
|
||||
to_field_name="suffix",
|
||||
label=_('Identity provider'),
|
||||
)
|
||||
#: The service url for which the user want a ticket
|
||||
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: A checkbox to remember the user choices of :attr:`provider<FederateSelect.provider>`
|
||||
remember = forms.BooleanField(label=_('Remember the identity provider'), required=False)
|
||||
#: A checkbox to ask to be warn before emiting a ticket for another service
|
||||
warn = forms.BooleanField(label=_('warn'), required=False)
|
||||
#: Is the service asking the authentication renewal ?
|
||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
|
||||
class UserCredential(forms.Form):
|
||||
"""Form used on the login page to retrive user credentials"""
|
||||
class UserCredential(BootsrapForm):
|
||||
"""
|
||||
Bases: :class:`django.forms.Form`
|
||||
|
||||
Form used on the login page to retrive user credentials
|
||||
"""
|
||||
#: The user username
|
||||
username = forms.CharField(label=_('login'))
|
||||
#: The service url for which the user want a ticket
|
||||
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
||||
#: The user password
|
||||
password = forms.CharField(label=_('password'), widget=forms.PasswordInput)
|
||||
#: A valid LoginTicket to prevent POST replay
|
||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: A checkbox to ask to be warn before emiting a ticket for another service
|
||||
warn = forms.BooleanField(label=_('warn'), required=False)
|
||||
#: Is the service asking the authentication renewal ?
|
||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UserCredential, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validate that the submited :attr:`username` and :attr:`password` are valid
|
||||
|
||||
:raises django.forms.ValidationError: if the :attr:`username` and :attr:`password`
|
||||
are not valid.
|
||||
:return: The cleaned POST data
|
||||
:rtype: dict
|
||||
"""
|
||||
cleaned_data = super(UserCredential, self).clean()
|
||||
auth = utils.import_attr(settings.CAS_AUTH_CLASS)(cleaned_data.get("username"))
|
||||
if auth.test_password(cleaned_data.get("password")):
|
||||
@ -73,17 +129,51 @@ class UserCredential(forms.Form):
|
||||
|
||||
|
||||
class FederateUserCredential(UserCredential):
|
||||
"""Form used on the login page to retrive user credentials"""
|
||||
"""
|
||||
Bases: :class:`UserCredential`
|
||||
|
||||
Form used on a auto submited page for linking the views
|
||||
:class:`FederateAuth<cas_server.views.FederateAuth>` and
|
||||
:class:`LoginView<cas_server.views.LoginView>`.
|
||||
|
||||
On successful authentication on a provider, in the view
|
||||
:class:`FederateAuth<cas_server.views.FederateAuth>` a
|
||||
:class:`FederatedUser<cas_server.models.FederatedUser>` is created by
|
||||
:meth:`cas_server.federate.CASFederateValidateUser.verify_ticket` and the user is redirected
|
||||
to :class:`LoginView<cas_server.views.LoginView>`. This form is then automatically filled
|
||||
with infos matching the created :class:`FederatedUser<cas_server.models.FederatedUser>`
|
||||
using the ``ticket`` as one time password and submited using javascript. If javascript is
|
||||
not enabled, a connect button is displayed.
|
||||
|
||||
This stub authentication form, allow to implement the federated mode with very few
|
||||
modificatons to the :class:`LoginView<cas_server.views.LoginView>` view.
|
||||
"""
|
||||
#: the user username with the ``@`` component
|
||||
username = forms.CharField(widget=forms.HiddenInput())
|
||||
#: The service url for which the user want a ticket
|
||||
service = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: The ``ticket`` used to authenticate the user against a provider
|
||||
password = forms.CharField(widget=forms.HiddenInput())
|
||||
#: alias of :attr:`password`
|
||||
ticket = forms.CharField(widget=forms.HiddenInput())
|
||||
#: A valid LoginTicket to prevent POST replay
|
||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: Has the user asked to be warn before emiting a ticket for another service
|
||||
warn = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
#: Is the service asking the authentication renewal ?
|
||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validate that the submited :attr:`username` and :attr:`password` are valid using
|
||||
the :class:`CASFederateAuth<cas_server.auth.CASFederateAuth>` auth class.
|
||||
|
||||
:raises django.forms.ValidationError: if the :attr:`username` and :attr:`password`
|
||||
do not correspond to a :class:`FederatedUser<cas_server.models.FederatedUser>`.
|
||||
:return: The cleaned POST data
|
||||
:rtype: dict
|
||||
"""
|
||||
cleaned_data = super(FederateUserCredential, self).clean()
|
||||
try:
|
||||
user = models.FederatedUser.get_from_federated_username(cleaned_data["username"])
|
||||
@ -99,7 +189,11 @@ class FederateUserCredential(UserCredential):
|
||||
|
||||
|
||||
class TicketForm(forms.ModelForm):
|
||||
"""Form for Tickets in the admin interface"""
|
||||
"""
|
||||
Bases: :class:`django.forms.ModelForm`
|
||||
|
||||
Form for Tickets in the admin interface
|
||||
"""
|
||||
class Meta:
|
||||
model = models.Ticket
|
||||
exclude = []
|
||||
|
@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
||||
from django.db import models, migrations
|
||||
import django.db.models.deletion
|
||||
import cas_server.utils
|
||||
import picklefield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -31,7 +30,7 @@ class Migration(migrations.Migration):
|
||||
name='ProxyGrantingTicket',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('attributs', picklefield.fields.PickledObjectField(editable=False)),
|
||||
('attributs', models.TextField(blank=True, default=None, null=True)),
|
||||
('validate', models.BooleanField(default=False)),
|
||||
('service', models.TextField()),
|
||||
('creation', models.DateTimeField(auto_now_add=True)),
|
||||
@ -47,7 +46,7 @@ class Migration(migrations.Migration):
|
||||
name='ProxyTicket',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('attributs', picklefield.fields.PickledObjectField(editable=False)),
|
||||
('attributs', models.TextField(blank=True, default=None, null=True)),
|
||||
('validate', models.BooleanField(default=False)),
|
||||
('service', models.TextField()),
|
||||
('creation', models.DateTimeField(auto_now_add=True)),
|
||||
@ -80,7 +79,7 @@ class Migration(migrations.Migration):
|
||||
name='ServiceTicket',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('attributs', picklefield.fields.PickledObjectField(editable=False)),
|
||||
('attributs', models.TextField(blank=True, default=None, null=True)),
|
||||
('validate', models.BooleanField(default=False)),
|
||||
('service', models.TextField()),
|
||||
('creation', models.DateTimeField(auto_now_add=True)),
|
||||
|
@ -3,7 +3,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import picklefield.fields
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@ -41,7 +40,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('username', models.CharField(max_length=124)),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cas_server.FederatedIendityProvider')),
|
||||
('attributs', picklefield.fields.PickledObjectField(editable=False)),
|
||||
('attributs', models.TextField(blank=True, default=None, null=True)),
|
||||
('ticket', models.CharField(max_length=255)),
|
||||
('last_update', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
|
40
cas_server/migrations/0006_auto_20160706_1727.py
Normal file
40
cas_server/migrations/0006_auto_20160706_1727.py
Normal file
@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-06 17:27
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cas_server', '0005_auto_20160616_1018'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='federatediendityprovider',
|
||||
name='cas_protocol_version',
|
||||
field=models.CharField(choices=[(b'1', b'CAS 1.0'), (b'2', b'CAS 2.0'), (b'3', b'CAS 3.0'), (b'CAS_2_SAML_1_0', b'SAML 1.1')], default=b'3', help_text='Version of the CAS protocol to use when sending requests the the backend CAS.', max_length=30, verbose_name='CAS protocol version'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='federatediendityprovider',
|
||||
name='display',
|
||||
field=models.BooleanField(default=True, help_text='Display the provider on the login page.', verbose_name='display'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='federatediendityprovider',
|
||||
name='pos',
|
||||
field=models.IntegerField(default=100, help_text='Position of the identity provider on the login page. Identity provider are sorted using the (position, verbose name, suffix) attributes.', verbose_name='position'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='federatediendityprovider',
|
||||
name='suffix',
|
||||
field=models.CharField(help_text='Suffix append to backend CAS returner username: ``returned_username`` @ ``suffix``.', max_length=30, unique=True, verbose_name='suffix'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='federatediendityprovider',
|
||||
name='verbose_name',
|
||||
field=models.CharField(help_text='Name for this identity provider displayed on the login page.', max_length=255, verbose_name='verbose name'),
|
||||
),
|
||||
]
|
56
cas_server/migrations/0007_auto_20160723_2252.py
Normal file
56
cas_server/migrations/0007_auto_20160723_2252.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-23 22:52
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cas_server', '0006_auto_20160706_1727'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='federateduser',
|
||||
name='attributs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='proxygrantingticket',
|
||||
name='attributs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='proxyticket',
|
||||
name='attributs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='serviceticket',
|
||||
name='attributs',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='federateduser',
|
||||
name='_attributs',
|
||||
field=models.TextField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='proxygrantingticket',
|
||||
name='_attributs',
|
||||
field=models.TextField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='proxyticket',
|
||||
name='_attributs',
|
||||
field=models.TextField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='serviceticket',
|
||||
name='_attributs',
|
||||
field=models.TextField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='federatediendityprovider',
|
||||
name='suffix',
|
||||
field=models.CharField(help_text='Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``.', max_length=30, unique=True, verbose_name='suffix'),
|
||||
),
|
||||
]
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
# (c) 2015-2016 Valentin Samir
|
||||
"""models for the app"""
|
||||
from .default_settings import settings
|
||||
from .default_settings import settings, SessionStore
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
@ -18,36 +18,46 @@ from django.contrib import messages
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from picklefield.fields import PickledObjectField
|
||||
|
||||
import re
|
||||
import sys
|
||||
import logging
|
||||
from importlib import import_module
|
||||
from datetime import timedelta
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from requests_futures.sessions import FuturesSession
|
||||
|
||||
import cas_server.utils as utils
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
#: logger facility
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class FederatedIendityProvider(models.Model):
|
||||
"""An identity provider for the federated mode"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
An identity provider for the federated mode
|
||||
"""
|
||||
class Meta:
|
||||
verbose_name = _("identity provider")
|
||||
verbose_name_plural = _("identity providers")
|
||||
verbose_name = _(u"identity provider")
|
||||
verbose_name_plural = _(u"identity providers")
|
||||
#: Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``.
|
||||
#: it must be unique.
|
||||
suffix = models.CharField(
|
||||
max_length=30,
|
||||
unique=True,
|
||||
verbose_name=_(u"suffix"),
|
||||
help_text=_("Suffix append to backend CAS returner username: `returned_username`@`suffix`")
|
||||
help_text=_(
|
||||
u"Suffix append to backend CAS returned "
|
||||
u"username: ``returned_username`` @ ``suffix``."
|
||||
)
|
||||
)
|
||||
#: URL to the root of the CAS server application. If login page is
|
||||
#: https://cas.example.net/cas/login then :attr:`server_url` should be
|
||||
#: https://cas.example.net/cas/
|
||||
server_url = models.CharField(max_length=255, verbose_name=_(u"server url"))
|
||||
#: Version of the CAS protocol to use when sending requests the the backend CAS.
|
||||
cas_protocol_version = models.CharField(
|
||||
max_length=30,
|
||||
choices=[
|
||||
@ -57,28 +67,37 @@ class FederatedIendityProvider(models.Model):
|
||||
("CAS_2_SAML_1_0", "SAML 1.1")
|
||||
],
|
||||
verbose_name=_(u"CAS protocol version"),
|
||||
help_text=_("Version of the CAS protocol to use when sending requests the the backend CAS"),
|
||||
help_text=_(
|
||||
u"Version of the CAS protocol to use when sending requests the the backend CAS."
|
||||
),
|
||||
default="3"
|
||||
)
|
||||
#: Name for this identity provider displayed on the login page.
|
||||
verbose_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"verbose name"),
|
||||
help_text=_("Name for this identity provider displayed on the login page")
|
||||
help_text=_(u"Name for this identity provider displayed on the login page.")
|
||||
)
|
||||
#: Position of the identity provider on the login page. Identity provider are sorted using the
|
||||
#: (:attr:`pos`, :attr:`verbose_name`, :attr:`suffix`) attributes.
|
||||
pos = models.IntegerField(
|
||||
default=100,
|
||||
verbose_name=_(u"position"),
|
||||
help_text=_(
|
||||
(
|
||||
u"Position of the identity provider on the login page. "
|
||||
u"Identity provider are sorted using the "
|
||||
u"(position, verbose name, suffix) attributes"
|
||||
u"(position, verbose name, suffix) attributes."
|
||||
)
|
||||
)
|
||||
)
|
||||
#: Display the provider on the login page. Beware that this do not disable the identity
|
||||
#: provider, it just hide it on the login page. User will always be able to log in using this
|
||||
#: provider by fetching ``/federate/suffix``.
|
||||
display = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_(u"display"),
|
||||
help_text=_("Display the provider on the login page")
|
||||
help_text=_("Display the provider on the login page.")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@ -86,36 +105,72 @@ class FederatedIendityProvider(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def build_username_from_suffix(username, suffix):
|
||||
"""Transform backend username into federated username using `suffix`"""
|
||||
"""
|
||||
Transform backend username into federated username using ``suffix``
|
||||
|
||||
:param unicode username: A CAS backend returned username
|
||||
:param unicode suffix: A suffix identifying the CAS backend
|
||||
:return: The federated username: ``username`` @ ``suffix``.
|
||||
:rtype: unicode
|
||||
"""
|
||||
return u'%s@%s' % (username, suffix)
|
||||
|
||||
def build_username(self, username):
|
||||
"""Transform backend username into federated username"""
|
||||
"""
|
||||
Transform backend username into federated username
|
||||
|
||||
:param unicode username: A CAS backend returned username
|
||||
:return: The federated username: ``username`` @ :attr:`suffix`.
|
||||
:rtype: unicode
|
||||
"""
|
||||
return u'%s@%s' % (username, self.suffix)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class FederatedUser(models.Model):
|
||||
"""A federated user as returner by a CAS provider (username and attributes)"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
A federated user as returner by a CAS provider (username and attributes)
|
||||
"""
|
||||
class Meta:
|
||||
unique_together = ("username", "provider")
|
||||
#: The user username returned by the CAS backend on successful ticket validation
|
||||
username = models.CharField(max_length=124)
|
||||
#: A foreign key to :class:`FederatedIendityProvider`
|
||||
provider = models.ForeignKey(FederatedIendityProvider, on_delete=models.CASCADE)
|
||||
attributs = PickledObjectField()
|
||||
#: The user attributes json encoded
|
||||
_attributs = models.TextField(default=None, null=True, blank=True)
|
||||
#: The last ticket used to authenticate :attr:`username` against :attr:`provider`
|
||||
ticket = models.CharField(max_length=255)
|
||||
#: Last update timespampt. Usually, the last time :attr:`ticket` has been set.
|
||||
last_update = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.federated_username
|
||||
|
||||
@property
|
||||
def attributs(self):
|
||||
"""The user attributes returned by the CAS backend on successful ticket validation"""
|
||||
if self._attributs is not None:
|
||||
return utils.json.loads(self._attributs)
|
||||
|
||||
@attributs.setter
|
||||
def attributs(self, value):
|
||||
"""attributs property setter"""
|
||||
self._attributs = utils.json_encode(value)
|
||||
|
||||
@property
|
||||
def federated_username(self):
|
||||
"""return the federated username with a suffix"""
|
||||
"""The federated username with a suffix for the current :class:`FederatedUser`."""
|
||||
return self.provider.build_username(self.username)
|
||||
|
||||
@classmethod
|
||||
def get_from_federated_username(cls, username):
|
||||
"""return a FederatedUser object from a federated username"""
|
||||
"""
|
||||
:return: A :class:`FederatedUser` object from a federated ``username``
|
||||
:rtype: :class:`FederatedUser`
|
||||
"""
|
||||
if username is None:
|
||||
raise cls.DoesNotExist()
|
||||
else:
|
||||
@ -130,7 +185,7 @@ class FederatedUser(models.Model):
|
||||
|
||||
@classmethod
|
||||
def clean_old_entries(cls):
|
||||
"""remove old unused federated users"""
|
||||
"""remove old unused :class:`FederatedUser`"""
|
||||
federated_users = cls.objects.filter(
|
||||
last_update__lt=(timezone.now() - timedelta(seconds=settings.CAS_TICKET_TIMEOUT))
|
||||
)
|
||||
@ -141,16 +196,23 @@ class FederatedUser(models.Model):
|
||||
|
||||
|
||||
class FederateSLO(models.Model):
|
||||
"""An association between a CAS provider ticket and a (username, session) for processing SLO"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
An association between a CAS provider ticket and a (username, session) for processing SLO
|
||||
"""
|
||||
class Meta:
|
||||
unique_together = ("username", "session_key", "ticket")
|
||||
#: the federated username with the ``@``component
|
||||
username = models.CharField(max_length=30)
|
||||
#: the session key for the session :attr:`username` has been authenticated using :attr:`ticket`
|
||||
session_key = models.CharField(max_length=40, blank=True, null=True)
|
||||
#: The ticket used to authenticate :attr:`username`
|
||||
ticket = models.CharField(max_length=255, db_index=True)
|
||||
|
||||
@classmethod
|
||||
def clean_deleted_sessions(cls):
|
||||
"""remove old object for which the session do not exists anymore"""
|
||||
"""remove old :class:`FederateSLO` object for which the session do not exists anymore"""
|
||||
for federate_slo in cls.objects.all():
|
||||
if not SessionStore(session_key=federate_slo.session_key).get('authenticated'):
|
||||
federate_slo.delete()
|
||||
@ -158,17 +220,27 @@ class FederateSLO(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class User(models.Model):
|
||||
"""A user logged into the CAS"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
A user logged into the CAS
|
||||
"""
|
||||
class Meta:
|
||||
unique_together = ("username", "session_key")
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
#: The session key of the current authenticated user
|
||||
session_key = models.CharField(max_length=40, blank=True, null=True)
|
||||
#: The username of the current authenticated user
|
||||
username = models.CharField(max_length=30)
|
||||
#: Last time the authenticated user has do something (auth, fetch ticket, etc…)
|
||||
date = models.DateTimeField(auto_now=True)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""remove the User"""
|
||||
"""
|
||||
Remove the current :class:`User`. If ``settings.CAS_FEDERATE`` is ``True``, also delete
|
||||
the corresponding :class:`FederateSLO` object.
|
||||
"""
|
||||
if settings.CAS_FEDERATE:
|
||||
FederateSLO.objects.filter(
|
||||
username=self.username,
|
||||
@ -178,7 +250,10 @@ class User(models.Model):
|
||||
|
||||
@classmethod
|
||||
def clean_old_entries(cls):
|
||||
"""Remove users inactive since more that SESSION_COOKIE_AGE"""
|
||||
"""
|
||||
Remove :class:`User` objects inactive since more that
|
||||
:django:setting:`SESSION_COOKIE_AGE` and send corresponding SingleLogOut requests.
|
||||
"""
|
||||
users = cls.objects.filter(
|
||||
date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE))
|
||||
)
|
||||
@ -188,7 +263,7 @@ class User(models.Model):
|
||||
|
||||
@classmethod
|
||||
def clean_deleted_sessions(cls):
|
||||
"""Remove user where the session do not exists anymore"""
|
||||
"""Remove :class:`User` objects where the corresponding session do not exists anymore."""
|
||||
for user in cls.objects.all():
|
||||
if not SessionStore(session_key=user.session_key).get('authenticated'):
|
||||
user.logout()
|
||||
@ -196,14 +271,22 @@ class User(models.Model):
|
||||
|
||||
@property
|
||||
def attributs(self):
|
||||
"""return a fresh dict for the user attributs"""
|
||||
"""
|
||||
Property.
|
||||
A fresh :class:`dict` for the user attributes, using ``settings.CAS_AUTH_CLASS``
|
||||
"""
|
||||
return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs()
|
||||
|
||||
def __str__(self):
|
||||
return u"%s - %s" % (self.username, self.session_key)
|
||||
|
||||
def logout(self, request=None):
|
||||
"""Sending SLO request to all services the user logged in"""
|
||||
"""
|
||||
Send SLO requests to all services the user is logged in.
|
||||
|
||||
:param request: The current django HttpRequest to display possible failure to the user.
|
||||
:type request: :class:`django.http.HttpRequest` or :obj:`NoneType<types.NoneType>`
|
||||
"""
|
||||
async_list = []
|
||||
session = FuturesSession(
|
||||
executor=ThreadPoolExecutor(max_workers=settings.CAS_SLO_MAX_PARALLEL_REQUESTS)
|
||||
@ -236,9 +319,22 @@ class User(models.Model):
|
||||
|
||||
def get_ticket(self, ticket_class, service, service_pattern, renew):
|
||||
"""
|
||||
Generate a ticket using `ticket_class` for the service
|
||||
`service` matching `service_pattern` and asking or not for
|
||||
authentication renewal with `renew
|
||||
Generate a ticket using ``ticket_class`` for the service
|
||||
``service`` matching ``service_pattern`` and asking or not for
|
||||
authentication renewal with ``renew``
|
||||
|
||||
:param type ticket_class: :class:`ServiceTicket` or :class:`ProxyTicket` or
|
||||
:class:`ProxyGrantingTicket`.
|
||||
:param unicode service: The service url for which we want a ticket.
|
||||
:param ServicePattern service_pattern: The service pattern matching ``service``.
|
||||
Beware that ``service`` must match :attr:`ServicePattern.pattern` and the current
|
||||
:class:`User` must pass :meth:`ServicePattern.check_user`. These checks are not done
|
||||
here and you must perform them before calling this method.
|
||||
:param bool renew: Should be ``True`` if authentication has been renewed. Must be
|
||||
``False`` otherwise.
|
||||
:return: A :class:`Ticket` object.
|
||||
:rtype: :class:`ServiceTicket` or :class:`ProxyTicket` or
|
||||
:class:`ProxyGrantingTicket`.
|
||||
"""
|
||||
attributs = dict(
|
||||
(a.name, a.replace if a.replace else a.name) for a in service_pattern.attributs.all()
|
||||
@ -273,8 +369,20 @@ class User(models.Model):
|
||||
return ticket
|
||||
|
||||
def get_service_url(self, service, service_pattern, renew):
|
||||
"""Return the url to which the user must be redirected to
|
||||
after a Service Ticket has been generated"""
|
||||
"""
|
||||
Return the url to which the user must be redirected to
|
||||
after a Service Ticket has been generated
|
||||
|
||||
:param unicode service: The service url for which we want a ticket.
|
||||
:param ServicePattern service_pattern: The service pattern matching ``service``.
|
||||
Beware that ``service`` must match :attr:`ServicePattern.pattern` and the current
|
||||
:class:`User` must pass :meth:`ServicePattern.check_user`. These checks are not done
|
||||
here and you must perform them before calling this method.
|
||||
:param bool renew: Should be ``True`` if authentication has been renewed. Must be
|
||||
``False`` otherwise.
|
||||
:return unicode: The service url with the ticket GET param added.
|
||||
:rtype: unicode
|
||||
"""
|
||||
ticket = self.get_ticket(ServiceTicket, service, service_pattern, renew)
|
||||
url = utils.update_url(service, {'ticket': ticket.value})
|
||||
logger.info("Service ticket created for service %s by user %s." % (service, self.username))
|
||||
@ -282,41 +390,60 @@ class User(models.Model):
|
||||
|
||||
|
||||
class ServicePatternException(Exception):
|
||||
"""Base exception of exceptions raised in the ServicePattern model"""
|
||||
"""
|
||||
Bases: :class:`exceptions.Exception`
|
||||
|
||||
Base exception of exceptions raised in the ServicePattern model"""
|
||||
pass
|
||||
|
||||
|
||||
class BadUsername(ServicePatternException):
|
||||
"""Exception raised then an non allowed username
|
||||
try to get a ticket for a service"""
|
||||
"""
|
||||
Bases: :class:`ServicePatternException`
|
||||
|
||||
Exception raised then an non allowed username try to get a ticket for a service
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BadFilter(ServicePatternException):
|
||||
""""Exception raised then a user try
|
||||
to get a ticket for a service and do not reach a condition"""
|
||||
"""
|
||||
Bases: :class:`ServicePatternException`
|
||||
|
||||
Exception raised then a user try to get a ticket for a service and do not reach a condition
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class UserFieldNotDefined(ServicePatternException):
|
||||
"""Exception raised then a user try to get a ticket for a service
|
||||
using as username an attribut not present on this user"""
|
||||
"""
|
||||
Bases: :class:`ServicePatternException`
|
||||
|
||||
Exception raised then a user try to get a ticket for a service using as username
|
||||
an attribut not present on this user
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ServicePattern(models.Model):
|
||||
"""Allowed services pattern agains services are tested to"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
Allowed services pattern agains services are tested to
|
||||
"""
|
||||
class Meta:
|
||||
ordering = ("pos", )
|
||||
verbose_name = _("Service pattern")
|
||||
verbose_name_plural = _("Services patterns")
|
||||
|
||||
#: service patterns are sorted using the :attr:`pos` attribute
|
||||
pos = models.IntegerField(
|
||||
default=100,
|
||||
verbose_name=_(u"position"),
|
||||
help_text=_(u"service patterns are sorted using the position attribute")
|
||||
)
|
||||
#: A name for the service (this can bedisplayed to the user on the login page)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
@ -325,6 +452,9 @@ class ServicePattern(models.Model):
|
||||
verbose_name=_(u"name"),
|
||||
help_text=_(u"A name for the service")
|
||||
)
|
||||
#: A regular expression matching services. "Will usually looks like
|
||||
#: '^https://some\\.server\\.com/path/.*$'. As it is a regular expression, special character
|
||||
#: must be escaped with a '\\'.
|
||||
pattern = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
@ -335,6 +465,7 @@ class ServicePattern(models.Model):
|
||||
"As it is a regular expression, special character must be escaped with a '\\'."
|
||||
)
|
||||
)
|
||||
#: Name of the attribut to transmit as username, if empty the user login is used
|
||||
user_field = models.CharField(
|
||||
max_length=255,
|
||||
default="",
|
||||
@ -342,27 +473,35 @@ class ServicePattern(models.Model):
|
||||
verbose_name=_(u"user field"),
|
||||
help_text=_("Name of the attribut to transmit as username, empty = login")
|
||||
)
|
||||
#: A boolean allowing to limit username allowed to connect to :attr:`usernames`.
|
||||
restrict_users = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_(u"restrict username"),
|
||||
help_text=_("Limit username allowed to connect to the list provided bellow")
|
||||
)
|
||||
#: A boolean allowing to deliver :class:`ProxyTicket` to the service.
|
||||
proxy = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_(u"proxy"),
|
||||
help_text=_("Proxy tickets can be delivered to the service")
|
||||
)
|
||||
#: A boolean allowing the service to be used as a proxy callback (via the pgtUrl GET param)
|
||||
#: to deliver :class:`ProxyGrantingTicket`.
|
||||
proxy_callback = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_(u"proxy callback"),
|
||||
help_text=_("can be used as a proxy callback to deliver PGT")
|
||||
)
|
||||
#: Enable SingleLogOut for the service. Old validaed tickets for the service will be kept
|
||||
#: until ``settings.CAS_TICKET_TIMEOUT`` after what a SLO request is send to the service and
|
||||
#: the ticket is purged from database. A SLO can be send earlier if the user log-out.
|
||||
single_log_out = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_(u"single log out"),
|
||||
help_text=_("Enable SLO for the service")
|
||||
)
|
||||
|
||||
#: An URL where the SLO request will be POST. If empty the service url will be used.
|
||||
#: This is usefull for non HTTP proxied services like smtp or imap.
|
||||
single_log_out_callback = models.CharField(
|
||||
max_length=255,
|
||||
default="",
|
||||
@ -376,7 +515,20 @@ class ServicePattern(models.Model):
|
||||
return u"%s: %s" % (self.pos, self.pattern)
|
||||
|
||||
def check_user(self, user):
|
||||
"""Check if `user` if allowed to use theses services"""
|
||||
"""
|
||||
Check if ``user`` if allowed to use theses services. If ``user`` is not allowed,
|
||||
raises one of :class:`BadFilter`, :class:`UserFieldNotDefined`, :class:`BadUsername`
|
||||
|
||||
:param User user: a :class:`User` object
|
||||
:raises BadUsername: if :attr:`restrict_users` if ``True`` and :attr:`User.username`
|
||||
is not within :attr:`usernames`.
|
||||
:raises BadFilter: if a :class:`FilterAttributValue` condition of :attr:`filters`
|
||||
connot be verified.
|
||||
:raises UserFieldNotDefined: if :attr:`user_field` is defined and its value is not
|
||||
within :attr:`User.attributs`.
|
||||
:return: ``True``
|
||||
:rtype: bool
|
||||
"""
|
||||
if self.restrict_users and not self.usernames.filter(value=user.username):
|
||||
logger.warning("Username %s not allowed on service %s" % (user.username, self.name))
|
||||
raise BadUsername()
|
||||
@ -416,8 +568,15 @@ class ServicePattern(models.Model):
|
||||
|
||||
@classmethod
|
||||
def validate(cls, service):
|
||||
"""Check if a Service Patern match `service` and
|
||||
return it, else raise `ServicePattern.DoesNotExist`"""
|
||||
"""
|
||||
Get a :class:`ServicePattern` intance from a service url.
|
||||
|
||||
:param unicode service: A service url
|
||||
:return: A :class:`ServicePattern` instance matching ``service``.
|
||||
:rtype: :class:`ServicePattern`
|
||||
:raises ServicePattern.DoesNotExist: if no :class:`ServicePattern` is matching
|
||||
``service``.
|
||||
"""
|
||||
for service_pattern in cls.objects.all().order_by('pos'):
|
||||
if re.match(service_pattern.pattern, service):
|
||||
return service_pattern
|
||||
@ -427,12 +586,20 @@ class ServicePattern(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Username(models.Model):
|
||||
"""A list of allowed usernames on a service pattern"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
A list of allowed usernames on a :class:`ServicePattern`
|
||||
"""
|
||||
#: username allowed to connect to the service
|
||||
value = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"username"),
|
||||
help_text=_(u"username allowed to connect to the service")
|
||||
)
|
||||
#: ForeignKey to a :class:`ServicePattern`. :class:`Username` instances for a
|
||||
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.usernames`
|
||||
#: attribute.
|
||||
service_pattern = models.ForeignKey(ServicePattern, related_name="usernames")
|
||||
|
||||
def __str__(self):
|
||||
@ -441,14 +608,23 @@ class Username(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ReplaceAttributName(models.Model):
|
||||
"""A list of replacement of attributs name for a service pattern"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
A replacement of an attribute name for a :class:`ServicePattern`. It also tell to transmit
|
||||
an attribute of :attr:`User.attributs` to the service. An empty :attr:`replace` mean
|
||||
to use the original attribute name.
|
||||
"""
|
||||
class Meta:
|
||||
unique_together = ('name', 'replace', 'service_pattern')
|
||||
#: Name the attribute: a key of :attr:`User.attributs`
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"name"),
|
||||
help_text=_(u"name of an attribut to send to the service, use * for all attributes")
|
||||
)
|
||||
#: The name of the attribute to transmit to the service. If empty, the value of :attr:`name`
|
||||
#: is used.
|
||||
replace = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
@ -456,6 +632,9 @@ class ReplaceAttributName(models.Model):
|
||||
help_text=_(u"name under which the attribut will be show"
|
||||
u"to the service. empty = default name of the attribut")
|
||||
)
|
||||
#: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributName` instances for a
|
||||
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.attributs`
|
||||
#: attribute.
|
||||
service_pattern = models.ForeignKey(ServicePattern, related_name="attributs")
|
||||
|
||||
def __str__(self):
|
||||
@ -467,17 +646,29 @@ class ReplaceAttributName(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class FilterAttributValue(models.Model):
|
||||
"""A list of filter on attributs for a service pattern"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
A filter on :attr:`User.attributs` for a :class:`ServicePattern`. If a :class:`User` do not
|
||||
have an attribute :attr:`attribut` or its value do not match :attr:`pattern`, then
|
||||
:meth:`ServicePattern.check_user` will raises :class:`BadFilter` if called with that user.
|
||||
"""
|
||||
#: The name of a user attribute
|
||||
attribut = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"attribut"),
|
||||
help_text=_(u"Name of the attribut which must verify pattern")
|
||||
)
|
||||
#: A regular expression the attribute :attr:`attribut` value must verify. If :attr:`attribut`
|
||||
#: if a list, only one of the list values needs to match.
|
||||
pattern = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"pattern"),
|
||||
help_text=_(u"a regular expression")
|
||||
)
|
||||
#: ForeignKey to a :class:`ServicePattern`. :class:`FilterAttributValue` instances for a
|
||||
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.filters`
|
||||
#: attribute.
|
||||
service_pattern = models.ForeignKey(ServicePattern, related_name="filters")
|
||||
|
||||
def __str__(self):
|
||||
@ -486,23 +677,34 @@ class FilterAttributValue(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ReplaceAttributValue(models.Model):
|
||||
"""Replacement to apply on attributs values for a service pattern"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
A replacement (using a regular expression) of an attribute value for a
|
||||
:class:`ServicePattern`.
|
||||
"""
|
||||
#: Name the attribute: a key of :attr:`User.attributs`
|
||||
attribut = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"attribut"),
|
||||
help_text=_(u"Name of the attribut for which the value must be replace")
|
||||
)
|
||||
#: A regular expression matching the part of the attribute value that need to be changed
|
||||
pattern = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"pattern"),
|
||||
help_text=_(u"An regular expression maching whats need to be replaced")
|
||||
)
|
||||
#: The replacement to what is mached by :attr:`pattern`. groups are capture by \\1, \\2 …
|
||||
replace = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
verbose_name=_(u"replace"),
|
||||
help_text=_(u"replace expression, groups are capture by \\1, \\2 …")
|
||||
)
|
||||
#: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributValue` instances for a
|
||||
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.replacements`
|
||||
#: attribute.
|
||||
service_pattern = models.ForeignKey(ServicePattern, related_name="replacements")
|
||||
|
||||
def __str__(self):
|
||||
@ -511,21 +713,54 @@ class ReplaceAttributValue(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Ticket(models.Model):
|
||||
"""Generic class for a Ticket"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
Generic class for a Ticket
|
||||
"""
|
||||
class Meta:
|
||||
abstract = True
|
||||
#: ForeignKey to a :class:`User`.
|
||||
user = models.ForeignKey(User, related_name="%(class)s")
|
||||
attributs = PickledObjectField()
|
||||
#: The user attributes to transmit to the service json encoded
|
||||
_attributs = models.TextField(default=None, null=True, blank=True)
|
||||
#: A boolean. ``True`` if the ticket has been validated
|
||||
validate = models.BooleanField(default=False)
|
||||
#: The service url for the ticket
|
||||
service = models.TextField()
|
||||
#: ForeignKey to a :class:`ServicePattern`. The :class:`ServicePattern` corresponding to
|
||||
#: :attr:`service`. Use :meth:`ServicePattern.validate` to find it.
|
||||
service_pattern = models.ForeignKey(ServicePattern, related_name="%(class)s")
|
||||
#: Date of the ticket creation
|
||||
creation = models.DateTimeField(auto_now_add=True)
|
||||
#: A boolean. ``True`` if the user has just renew his authentication
|
||||
renew = models.BooleanField(default=False)
|
||||
#: A boolean. Set to :attr:`service_pattern` attribute
|
||||
#: :attr:`ServicePattern.single_log_out` value.
|
||||
single_log_out = models.BooleanField(default=False)
|
||||
|
||||
#: Max duration between ticket creation and its validation. Any validation attempt for the
|
||||
#: ticket after :attr:`creation` + VALIDITY will fail as if the ticket do not exists.
|
||||
VALIDITY = settings.CAS_TICKET_VALIDITY
|
||||
#: Time we keep ticket with :attr:`single_log_out` set to ``True`` before sending SingleLogOut
|
||||
#: requests.
|
||||
TIMEOUT = settings.CAS_TICKET_TIMEOUT
|
||||
|
||||
@property
|
||||
def attributs(self):
|
||||
"""The user attributes to be transmited to the service on successful validation"""
|
||||
if self._attributs is not None:
|
||||
return utils.json.loads(self._attributs)
|
||||
|
||||
@attributs.setter
|
||||
def attributs(self, value):
|
||||
"""attributs property setter"""
|
||||
self._attributs = utils.json_encode(value)
|
||||
|
||||
class DoesNotExist(Exception):
|
||||
"""raised in :meth:`Ticket.get` then ticket prefix and ticket classes mismatch"""
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return u"Ticket-%s" % self.pk
|
||||
|
||||
@ -596,16 +831,119 @@ class Ticket(models.Model):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_class(ticket):
|
||||
for ticket_class in [ServiceTicket, ProxyTicket, ProxyGrantingTicket]:
|
||||
def get_class(ticket, classes=None):
|
||||
"""
|
||||
Return the ticket class of ``ticket``
|
||||
|
||||
:param unicode ticket: A ticket
|
||||
:param list classes: Optinal arguement. A list of possible :class:`Ticket` subclasses
|
||||
:return: The class corresponding to ``ticket`` (:class:`ServiceTicket` or
|
||||
:class:`ProxyTicket` or :class:`ProxyGrantingTicket`) if found among ``classes,
|
||||
``None`` otherwise.
|
||||
:rtype: :obj:`type` or :obj:`NoneType<types.NoneType>`
|
||||
"""
|
||||
if classes is None: # pragma: no cover (not used)
|
||||
classes = [ServiceTicket, ProxyTicket, ProxyGrantingTicket]
|
||||
for ticket_class in classes:
|
||||
if ticket.startswith(ticket_class.PREFIX):
|
||||
return ticket_class
|
||||
|
||||
def username(self):
|
||||
"""
|
||||
The username to send on ticket validation
|
||||
|
||||
:return: The value of the corresponding user attribute if
|
||||
:attr:`service_pattern`.user_field is set, the user username otherwise.
|
||||
"""
|
||||
if self.service_pattern.user_field and self.user.attributs.get(
|
||||
self.service_pattern.user_field
|
||||
):
|
||||
username = self.user.attributs[self.service_pattern.user_field]
|
||||
if isinstance(username, list):
|
||||
# the list is not empty because we wont generate a ticket with a user_field
|
||||
# that evaluate to False
|
||||
username = username[0]
|
||||
else:
|
||||
username = self.user.username
|
||||
return username
|
||||
|
||||
def attributs_flat(self):
|
||||
"""
|
||||
generate attributes list for template rendering
|
||||
|
||||
:return: An list of (attribute name, attribute value) of all user attributes flatened
|
||||
(no nested list)
|
||||
:rtype: :obj:`list` of :obj:`tuple` of :obj:`unicode`
|
||||
"""
|
||||
attributes = []
|
||||
for key, value in self.attributs.items():
|
||||
if isinstance(value, list):
|
||||
for elt in value:
|
||||
attributes.append((key, elt))
|
||||
else:
|
||||
attributes.append((key, value))
|
||||
return attributes
|
||||
|
||||
@classmethod
|
||||
def get(cls, ticket, renew=False, service=None):
|
||||
"""
|
||||
Search the database for a valid ticket with provided arguments
|
||||
|
||||
:param unicode ticket: A ticket value
|
||||
:param bool renew: Is authentication renewal needed
|
||||
:param unicode service: Optional argument. The ticket service
|
||||
:raises Ticket.DoesNotExist: if no class is found for the ticket prefix
|
||||
:raises cls.DoesNotExist: if ``ticket`` value is not found in th database
|
||||
:return: a :class:`Ticket` instance
|
||||
:rtype: Ticket
|
||||
"""
|
||||
# If the method class is the ticket abstract class, search for the submited ticket
|
||||
# class using its prefix. Assuming ticket is a ProxyTicket or a ServiceTicket
|
||||
if cls == Ticket:
|
||||
ticket_class = cls.get_class(ticket, classes=[ServiceTicket, ProxyTicket])
|
||||
# else use the method class
|
||||
else:
|
||||
ticket_class = cls
|
||||
# If ticket prefix is wrong, raise DoesNotExist
|
||||
if cls != Ticket and not ticket.startswith(cls.PREFIX):
|
||||
raise Ticket.DoesNotExist()
|
||||
if ticket_class:
|
||||
# search for the ticket that is not yet validated and is still valid
|
||||
ticket_queryset = ticket_class.objects.filter(
|
||||
value=ticket,
|
||||
validate=False,
|
||||
creation__gt=(timezone.now() - timedelta(seconds=ticket_class.VALIDITY))
|
||||
)
|
||||
# if service is specified, add it the the queryset
|
||||
if service is not None:
|
||||
ticket_queryset = ticket_queryset.filter(service=service)
|
||||
# only require renew if renew is True, otherwise it do not matter if renew is True
|
||||
# or False.
|
||||
if renew:
|
||||
ticket_queryset = ticket_queryset.filter(renew=True)
|
||||
# fetch the ticket ``MultipleObjectsReturned`` is never raised as the ticket value
|
||||
# is unique across the database
|
||||
ticket = ticket_queryset.get()
|
||||
# For ServiceTicket and Proxyticket, mark it as validated before returning
|
||||
if ticket_class != ProxyGrantingTicket:
|
||||
ticket.validate = True
|
||||
ticket.save()
|
||||
return ticket
|
||||
# If no class found for the ticket, raise DoesNotExist
|
||||
else:
|
||||
raise Ticket.DoesNotExist()
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ServiceTicket(Ticket):
|
||||
"""A Service Ticket"""
|
||||
"""
|
||||
Bases: :class:`Ticket`
|
||||
|
||||
A Service Ticket
|
||||
"""
|
||||
#: The ticket prefix used to differentiate it from other tickets types
|
||||
PREFIX = settings.CAS_SERVICE_TICKET_PREFIX
|
||||
#: The ticket value
|
||||
value = models.CharField(max_length=255, default=utils.gen_st, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
@ -614,8 +952,14 @@ class ServiceTicket(Ticket):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ProxyTicket(Ticket):
|
||||
"""A Proxy Ticket"""
|
||||
"""
|
||||
Bases: :class:`Ticket`
|
||||
|
||||
A Proxy Ticket
|
||||
"""
|
||||
#: The ticket prefix used to differentiate it from other tickets types
|
||||
PREFIX = settings.CAS_PROXY_TICKET_PREFIX
|
||||
#: The ticket value
|
||||
value = models.CharField(max_length=255, default=utils.gen_pt, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
@ -624,9 +968,17 @@ class ProxyTicket(Ticket):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ProxyGrantingTicket(Ticket):
|
||||
"""A Proxy Granting Ticket"""
|
||||
"""
|
||||
Bases: :class:`Ticket`
|
||||
|
||||
A Proxy Granting Ticket
|
||||
"""
|
||||
#: The ticket prefix used to differentiate it from other tickets types
|
||||
PREFIX = settings.CAS_PROXY_GRANTING_TICKET_PREFIX
|
||||
#: ProxyGranting ticket are never validated. However, they can be used during :attr:`VALIDITY`
|
||||
#: to get :class:`ProxyTicket` for :attr:`user`
|
||||
VALIDITY = settings.CAS_PGT_VALIDITY
|
||||
#: The ticket value
|
||||
value = models.CharField(max_length=255, default=utils.gen_pgt, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
@ -635,10 +987,18 @@ class ProxyGrantingTicket(Ticket):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Proxy(models.Model):
|
||||
"""A list of proxies on `ProxyTicket`"""
|
||||
"""
|
||||
Bases: :class:`django.db.models.Model`
|
||||
|
||||
A list of proxies on :class:`ProxyTicket`
|
||||
"""
|
||||
class Meta:
|
||||
ordering = ("-pk", )
|
||||
#: Service url of the PGT used for getting the associated :class:`ProxyTicket`
|
||||
url = models.CharField(max_length=255)
|
||||
#: ForeignKey to a :class:`ProxyTicket`. :class:`Proxy` instances for a
|
||||
#: :class:`ProxyTicket` are accessible thought its :attr:`ProxyTicket.proxies`
|
||||
#: attribute.
|
||||
proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies")
|
||||
|
||||
def __str__(self):
|
||||
|
@ -1,36 +1,63 @@
|
||||
{% extends 'bootstrap3/bootstrap3.html' %}
|
||||
{% load i18n %}
|
||||
{% block bootstrap3_title %}{% block title %}{% trans "Central Authentication Service" %}{% endblock %}{% endblock %}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block bootstrap3_extra_head %}
|
||||
<link rel="shortcut icon" href="/static/cas_server/favicon.ico?v=1" />
|
||||
<link href="{% static "cas_server/login.css" %}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block bootstrap3_content %}
|
||||
<div class="container">
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
<div class="row">
|
||||
<!DOCTYPE html>
|
||||
<html{% if request.LANGUAGE_CODE %} lang="{{ request.LANGUAGE_CODE }}"{% endif %}>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge" /><![endif]-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}{% trans "Central Authentication Service" %}{% endblock %}</title>
|
||||
<link href="{{settings.CAS_COMPONENT_URLS.bootstrap3_css}}" rel="stylesheet">
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="{{settings.CAS_COMPONENT_URLS.html5shiv}}"></script>
|
||||
<script src="{{settings.CAS_COMPONENT_URLS.respond}}"></script>
|
||||
<![endif]-->
|
||||
<link rel="shortcut icon" href="{% static "cas_server/favicon.ico?v=1" %}" />
|
||||
<link href="{% static "cas_server/login.css" %}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<h1 id="app-name">
|
||||
{% if settings.CAS_LOGO_URL %}<img src="{{settings.CAS_LOGO_URL}}"></img> {% endif %}
|
||||
{% trans "Central Authentication Service" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{% if auto_submit %}</noscript>{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
{% bootstrap_messages %}
|
||||
{% if auto_submit %}</noscript>{% endif %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% if auto_submit %}</noscript>{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
|
||||
{% block ante_messages %}{% endblock %}
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
{% for message in messages %}
|
||||
<div {% spaceless %}
|
||||
{% if message.level == message_levels.DEBUG %}
|
||||
class="alert alert-warning alert-dismissable"
|
||||
{% elif message.level == message_levels.INFO %}
|
||||
class="alert alert-info alert-dismissable"
|
||||
{% elif message.level == message_levels.SUCCESS %}
|
||||
class="alert alert-success alert-dismissable"
|
||||
{% elif message.level == message_levels.WARNING %}
|
||||
class="alert alert-warning alert-dismissable"
|
||||
{% else %}
|
||||
class="alert alert-danger alert-dismissable"
|
||||
{% endif %}
|
||||
{% endspaceless %}>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if auto_submit %}</noscript>{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-0"></div>
|
||||
</div>
|
||||
</div> <!-- /container -->
|
||||
{% endblock %}
|
||||
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
|
||||
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
25
cas_server/templates/cas_server/form.html
Normal file
25
cas_server/templates/cas_server/form.html
Normal file
@ -0,0 +1,25 @@
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{{error}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for field in form %}{% if field.display %}
|
||||
<div class="form-group{% spaceless %}
|
||||
{% if not form.non_field_errors %}
|
||||
{% if field.errors %} has-error
|
||||
{% elif form.cleaned_data %} has-success
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
{% endspaceless %}>{% spaceless %}
|
||||
{% if field.checkbox %}
|
||||
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label>
|
||||
{% else %}
|
||||
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
|
||||
{{field}}
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<span class="help-block">{{error}}</span>
|
||||
{% endfor %}
|
||||
{% endspaceless %}</div>
|
||||
{% else %}{{field}}{% endif %}{% endfor %}
|
@ -1,6 +1,4 @@
|
||||
{% extends "cas_server/base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="alert alert-success" role="alert">{% trans "Logged" %}</div>
|
||||
@ -10,7 +8,7 @@
|
||||
<input type="checkbox" name="all" value="1"> {% trans "Log me out from all my sessions" %}
|
||||
</label>
|
||||
</div>
|
||||
{% bootstrap_button _('Logout') size='lg' button_type="submit" button_class="btn-danger btn-block"%}
|
||||
<button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1,18 +1,19 @@
|
||||
{% extends "cas_server/base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block ante_messages %}
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
<h2 class="form-signin-heading">{% trans "Please log in" %}</h2>
|
||||
{% if auto_submit %}</noscript>{% endif %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
<h2 class="form-signin-heading">{% trans "Please log in" %}</h2>
|
||||
{% if auto_submit %}</noscript>{% endif %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
{% bootstrap_button _('Login') size='lg' button_type="submit" button_class="btn-primary btn-block"%}
|
||||
{% if auto_submit %}</noscript>{% endif %}
|
||||
</form>
|
||||
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
|
||||
{% csrf_token %}
|
||||
{% include "cas_server/form.html" %}
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button>
|
||||
{% if auto_submit %}</noscript>{% endif %}
|
||||
</form>
|
||||
{% if auto_submit %}
|
||||
<script type="text/javascript">
|
||||
document.getElementById('login_form').submit(); // SUBMIT FORM
|
||||
|
@ -1,5 +1,4 @@
|
||||
{% extends "cas_server/base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
|
@ -1,12 +1,11 @@
|
||||
{% extends "cas_server/base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form class="form-signin" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_button _('Connect to the service') size='lg' button_type="submit" button_class="btn-primary btn-block"%}
|
||||
{% include "cas_server/form.html" %}
|
||||
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Connect to the service" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
@ -37,7 +37,6 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'bootstrap3',
|
||||
'cas_server',
|
||||
]
|
||||
|
||||
|
@ -993,7 +993,7 @@ class ValidateTestCase(TestCase):
|
||||
def test_validate_service_renew(self):
|
||||
"""test with a valid (ticket, service) asking for auth renewal"""
|
||||
# case 1 client is renewing and service ask for renew
|
||||
(client1, response) = get_auth_client(renew="True", service=self.service)
|
||||
response = get_auth_client(renew="True", service=self.service)[1]
|
||||
self.assertEqual(response.status_code, 302)
|
||||
ticket_value = response['Location'].split('ticket=')[-1]
|
||||
# get a bare client
|
||||
@ -1112,7 +1112,9 @@ class ValidateServiceTestCase(TestCase, XmlContent):
|
||||
name="localhost",
|
||||
pattern="^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$",
|
||||
# allow to request PGT by the service
|
||||
proxy_callback=True
|
||||
proxy_callback=True,
|
||||
# allow to request PT for the service
|
||||
proxy=True
|
||||
)
|
||||
# tell the service pattern to transmit all the user attributes (* is a joker)
|
||||
models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern)
|
||||
@ -1190,10 +1192,30 @@ class ValidateServiceTestCase(TestCase, XmlContent):
|
||||
# the attributes settings.CAS_TEST_ATTRIBUTES
|
||||
self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES)
|
||||
|
||||
def test_validate_proxy(self):
|
||||
"""test ProxyTicket validation on /proxyValidate and /serviceValidate"""
|
||||
ticket = get_proxy_ticket(self.service)
|
||||
client = Client()
|
||||
# requesting validation with a good (ticket, service)
|
||||
response = client.get('/proxyValidate', {'ticket': ticket.value, 'service': self.service})
|
||||
# and it should succeed
|
||||
self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES)
|
||||
|
||||
ticket = get_proxy_ticket(self.service)
|
||||
client = Client()
|
||||
# requesting validation with a good (ticket, service)
|
||||
response = client.get('/serviceValidate', {'ticket': ticket.value, 'service': self.service})
|
||||
# and it should succeed
|
||||
self.assert_error(
|
||||
response,
|
||||
"INVALID_TICKET",
|
||||
ticket.value
|
||||
)
|
||||
|
||||
def test_validate_service_renew(self):
|
||||
"""test with a valid (ticket, service) asking for auth renewal"""
|
||||
# case 1 client is renewing and service ask for renew
|
||||
(client1, response) = get_auth_client(renew="True", service=self.service)
|
||||
response = get_auth_client(renew="True", service=self.service)[1]
|
||||
self.assertEqual(response.status_code, 302)
|
||||
ticket_value = response['Location'].split('ticket=')[-1]
|
||||
# get a bare client
|
||||
|
@ -10,14 +10,13 @@
|
||||
#
|
||||
# (c) 2015-2016 Valentin Samir
|
||||
"""urls for the app"""
|
||||
from django.conf.urls import patterns, url
|
||||
from django.conf.urls import url
|
||||
from django.views.generic import RedirectView
|
||||
from django.views.decorators.debug import sensitive_post_parameters, sensitive_variables
|
||||
|
||||
from cas_server import views
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
urlpatterns = [
|
||||
url(r'^$', RedirectView.as_view(pattern_name="cas_server:login")),
|
||||
url(
|
||||
'^login$',
|
||||
@ -60,4 +59,4 @@ urlpatterns = patterns(
|
||||
name='auth'
|
||||
),
|
||||
url("^federate(?:/(?P<provider>([^/]+)))?$", views.FederateAuth.as_view(), name='federateAuth'),
|
||||
)
|
||||
]
|
||||
|
@ -15,6 +15,8 @@ from .default_settings import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages import constants as DEFAULT_MESSAGE_LEVELS
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
import random
|
||||
import string
|
||||
@ -29,14 +31,38 @@ from datetime import datetime, timedelta
|
||||
from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
|
||||
|
||||
|
||||
def json_encode(obj):
|
||||
"""Encode a python object to json"""
|
||||
try:
|
||||
return json_encode.encoder.encode(obj)
|
||||
except AttributeError:
|
||||
json_encode.encoder = DjangoJSONEncoder(default=six.text_type)
|
||||
return json_encode(obj)
|
||||
|
||||
|
||||
def context(params):
|
||||
"""Function that add somes variable to the context before template rendering"""
|
||||
"""
|
||||
Function that add somes variable to the context before template rendering
|
||||
|
||||
:param dict params: The context dictionary used to render templates.
|
||||
:return: The ``params`` dictionary with the key ``settings`` set to
|
||||
:obj:`django.conf.settings`.
|
||||
:rtype: dict
|
||||
"""
|
||||
params["settings"] = settings
|
||||
params["message_levels"] = DEFAULT_MESSAGE_LEVELS
|
||||
return params
|
||||
|
||||
|
||||
def json_response(request, data):
|
||||
"""Wrapper dumping `data` to a json and sending it to the user with an HttpResponse"""
|
||||
"""
|
||||
Wrapper dumping `data` to a json and sending it to the user with an HttpResponse
|
||||
|
||||
:param django.http.HttpRequest request: The request object used to generate this response.
|
||||
:param dict data: The python dictionnary to return as a json
|
||||
:return: The content of ``data`` serialized in json
|
||||
:rtype: django.http.HttpResponse
|
||||
"""
|
||||
data["messages"] = []
|
||||
for msg in messages.get_messages(request):
|
||||
data["messages"].append({'message': msg.message, 'level': msg.level_tag})
|
||||
@ -44,7 +70,13 @@ def json_response(request, data):
|
||||
|
||||
|
||||
def import_attr(path):
|
||||
"""transform a python module.attr path to the attr"""
|
||||
"""
|
||||
transform a python dotted path to the attr
|
||||
|
||||
:param path: A dotted path to a python object or a python object
|
||||
:type path: :obj:`unicode` or anything
|
||||
:return: The python object pointed by the dotted path or the python object unchanged
|
||||
"""
|
||||
if not isinstance(path, str):
|
||||
return path
|
||||
if "." not in path:
|
||||
@ -59,24 +91,50 @@ def import_attr(path):
|
||||
|
||||
|
||||
def redirect_params(url_name, params=None):
|
||||
"""Redirect to `url_name` with `params` as querystring"""
|
||||
"""
|
||||
Redirect to ``url_name`` with ``params`` as querystring
|
||||
|
||||
:param unicode url_name: a URL pattern name
|
||||
:param params: Some parameter to append to the reversed URL
|
||||
:type params: :obj:`dict` or :obj:`NoneType<types.NoneType>`
|
||||
:return: A redirection to the URL with name ``url_name`` with ``params`` as querystring.
|
||||
:rtype: django.http.HttpResponseRedirect
|
||||
"""
|
||||
url = reverse(url_name)
|
||||
params = urlencode(params if params else {})
|
||||
return HttpResponseRedirect(url + "?%s" % params)
|
||||
|
||||
|
||||
def reverse_params(url_name, params=None, **kwargs):
|
||||
"""compule the reverse url or `url_name` and add GET parameters from `params` to it"""
|
||||
"""
|
||||
compute the reverse url of ``url_name`` and add to it parameters from ``params``
|
||||
as querystring
|
||||
|
||||
:param unicode url_name: a URL pattern name
|
||||
:param params: Some parameter to append to the reversed URL
|
||||
:type params: :obj:`dict` or :obj:`NoneType<types.NoneType>`
|
||||
:param **kwargs: additional parameters needed to compure the reverse URL
|
||||
:return: The computed reverse URL of ``url_name`` with possible querystring from ``params``
|
||||
:rtype: unicode
|
||||
"""
|
||||
url = reverse(url_name, **kwargs)
|
||||
params = urlencode(params if params else {})
|
||||
if params:
|
||||
return url + "?%s" % params
|
||||
return u"%s?%s" % (url, params)
|
||||
else:
|
||||
return url
|
||||
|
||||
|
||||
def copy_params(get_or_post_params, ignore=None):
|
||||
"""copy from a dictionnary like `get_or_post_params` ignoring keys in the set `ignore`"""
|
||||
"""
|
||||
copy a :class:`django.http.QueryDict` in a :obj:`dict` ignoring keys in the set ``ignore``
|
||||
|
||||
:param django.http.QueryDict get_or_post_params: A GET or POST
|
||||
:class:`QueryDict<django.http.QueryDict>`
|
||||
:param set ignore: An optinal set of keys to ignore during the copy
|
||||
:return: A copy of get_or_post_params
|
||||
:rtype: dict
|
||||
"""
|
||||
if ignore is None:
|
||||
ignore = set()
|
||||
params = {}
|
||||
@ -87,7 +145,14 @@ def copy_params(get_or_post_params, ignore=None):
|
||||
|
||||
|
||||
def set_cookie(response, key, value, max_age):
|
||||
"""Set the cookie `key` on `response` with value `value` valid for `max_age` secondes"""
|
||||
"""
|
||||
Set the cookie ``key`` on ``response`` with value ``value`` valid for ``max_age`` secondes
|
||||
|
||||
:param django.http.HttpResponse response: a django response where to set the cookie
|
||||
:param unicode key: the cookie key
|
||||
:param unicode value: the cookie value
|
||||
:param int max_age: the maximum validity age of the cookie
|
||||
"""
|
||||
expires = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(seconds=max_age),
|
||||
"%a, %d-%b-%Y %H:%M:%S GMT"
|
||||
@ -103,20 +168,36 @@ def set_cookie(response, key, value, max_age):
|
||||
|
||||
|
||||
def get_current_url(request, ignore_params=None):
|
||||
"""Giving a django request, return the current http url, possibly ignoring some GET params"""
|
||||
"""
|
||||
Giving a django request, return the current http url, possibly ignoring some GET parameters
|
||||
|
||||
:param django.http.HttpRequest request: The current request object.
|
||||
:param set ignore_params: An optional set of GET parameters to ignore
|
||||
:return: The URL of the current page, possibly omitting some parameters from
|
||||
``ignore_params`` in the querystring.
|
||||
:rtype: unicode
|
||||
"""
|
||||
if ignore_params is None:
|
||||
ignore_params = set()
|
||||
protocol = 'https' if request.is_secure() else "http"
|
||||
service_url = "%s://%s%s" % (protocol, request.get_host(), request.path)
|
||||
protocol = u'https' if request.is_secure() else u"http"
|
||||
service_url = u"%s://%s%s" % (protocol, request.get_host(), request.path)
|
||||
if request.GET:
|
||||
params = copy_params(request.GET, ignore_params)
|
||||
if params:
|
||||
service_url += "?%s" % urlencode(params)
|
||||
service_url += u"?%s" % urlencode(params)
|
||||
return service_url
|
||||
|
||||
|
||||
def update_url(url, params):
|
||||
"""update params in the `url` query string"""
|
||||
"""
|
||||
update parameters using ``params`` in the ``url`` query string
|
||||
|
||||
:param url: An URL possibily with a querystring
|
||||
:type url: :obj:`unicode` or :obj:`str`
|
||||
:param dict params: A dictionary of parameters for updating the url querystring
|
||||
:return: The URL with an updated querystring
|
||||
:rtype: unicode
|
||||
"""
|
||||
if not isinstance(url, bytes):
|
||||
url = url.encode('utf-8')
|
||||
for key, value in list(params.items()):
|
||||
@ -140,7 +221,12 @@ def update_url(url, params):
|
||||
|
||||
|
||||
def unpack_nested_exception(error):
|
||||
"""If exception are stacked, return the first one"""
|
||||
"""
|
||||
If exception are stacked, return the first one
|
||||
|
||||
:param error: A python exception with possible exception embeded within
|
||||
:return: A python exception with no exception embeded within
|
||||
"""
|
||||
i = 0
|
||||
while True:
|
||||
if error.args[i:]:
|
||||
@ -154,52 +240,97 @@ def unpack_nested_exception(error):
|
||||
return error
|
||||
|
||||
|
||||
def _gen_ticket(prefix, lg=settings.CAS_TICKET_LEN):
|
||||
"""Generate a ticket with prefix `prefix`"""
|
||||
return '%s-%s' % (
|
||||
prefix,
|
||||
''.join(
|
||||
def _gen_ticket(prefix=None, lg=settings.CAS_TICKET_LEN):
|
||||
"""
|
||||
Generate a ticket with prefix ``prefix`` and length ``lg``
|
||||
|
||||
:param unicode prefix: An optional prefix (probably ST, PT, PGT or PGTIOU)
|
||||
:param int lg: The length of the generated ticket (with the prefix)
|
||||
:return: A randomlly generated ticket of length ``lg``
|
||||
:rtype: unicode
|
||||
"""
|
||||
random_part = u''.join(
|
||||
random.choice(
|
||||
string.ascii_letters + string.digits
|
||||
) for _ in range(lg - len(prefix) - 1)
|
||||
)
|
||||
) for _ in range(lg - len(prefix or "") - 1)
|
||||
)
|
||||
if prefix is not None:
|
||||
return u'%s-%s' % (prefix, random_part)
|
||||
else:
|
||||
return random_part
|
||||
|
||||
|
||||
def gen_lt():
|
||||
"""Generate a Service Ticket"""
|
||||
"""
|
||||
Generate a Login Ticket
|
||||
|
||||
:return: A ticket with prefix ``settings.CAS_LOGIN_TICKET_PREFIX`` and length
|
||||
``settings.CAS_LT_LEN``
|
||||
:rtype: unicode
|
||||
"""
|
||||
return _gen_ticket(settings.CAS_LOGIN_TICKET_PREFIX, settings.CAS_LT_LEN)
|
||||
|
||||
|
||||
def gen_st():
|
||||
"""Generate a Service Ticket"""
|
||||
"""
|
||||
Generate a Service Ticket
|
||||
|
||||
:return: A ticket with prefix ``settings.CAS_SERVICE_TICKET_PREFIX`` and length
|
||||
``settings.CAS_ST_LEN``
|
||||
:rtype: unicode
|
||||
"""
|
||||
return _gen_ticket(settings.CAS_SERVICE_TICKET_PREFIX, settings.CAS_ST_LEN)
|
||||
|
||||
|
||||
def gen_pt():
|
||||
"""Generate a Proxy Ticket"""
|
||||
"""
|
||||
Generate a Proxy Ticket
|
||||
|
||||
:return: A ticket with prefix ``settings.CAS_PROXY_TICKET_PREFIX`` and length
|
||||
``settings.CAS_PT_LEN``
|
||||
:rtype: unicode
|
||||
"""
|
||||
return _gen_ticket(settings.CAS_PROXY_TICKET_PREFIX, settings.CAS_PT_LEN)
|
||||
|
||||
|
||||
def gen_pgt():
|
||||
"""Generate a Proxy Granting Ticket"""
|
||||
"""
|
||||
Generate a Proxy Granting Ticket
|
||||
|
||||
:return: A ticket with prefix ``settings.CAS_PROXY_GRANTING_TICKET_PREFIX`` and length
|
||||
``settings.CAS_PGT_LEN``
|
||||
:rtype: unicode
|
||||
"""
|
||||
return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_PREFIX, settings.CAS_PGT_LEN)
|
||||
|
||||
|
||||
def gen_pgtiou():
|
||||
"""Generate a Proxy Granting Ticket IOU"""
|
||||
"""
|
||||
Generate a Proxy Granting Ticket IOU
|
||||
|
||||
:return: A ticket with prefix ``settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX`` and length
|
||||
``settings.CAS_PGTIOU_LEN``
|
||||
:rtype: unicode
|
||||
"""
|
||||
return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX, settings.CAS_PGTIOU_LEN)
|
||||
|
||||
|
||||
def gen_saml_id():
|
||||
"""Generate an saml id"""
|
||||
return _gen_ticket('_')
|
||||
"""
|
||||
Generate an saml id
|
||||
|
||||
:return: A random id of length ``settings.CAS_TICKET_LEN``
|
||||
:rtype: unicode
|
||||
"""
|
||||
return _gen_ticket()
|
||||
|
||||
|
||||
def get_tuple(nuplet, index, default=None):
|
||||
"""
|
||||
return the value in index `index` of the tuple `nuplet` if it exists,
|
||||
else return `default`
|
||||
:param tuple nuplet: A tuple
|
||||
:param int index: An index
|
||||
:param default: An optional default value
|
||||
:return: ``nuplet[index]`` if defined, else ``default`` (possibly ``None``)
|
||||
"""
|
||||
if nuplet is None:
|
||||
return default
|
||||
@ -210,7 +341,13 @@ def get_tuple(nuplet, index, default=None):
|
||||
|
||||
|
||||
def crypt_salt_is_valid(salt):
|
||||
"""Return True is salt is valid has a crypt salt, False otherwise"""
|
||||
"""
|
||||
Validate a salt as crypt salt
|
||||
|
||||
:param str salt: a password salt
|
||||
:return: ``True`` if ``salt`` is a valid crypt salt on this system, ``False`` otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
if len(salt) < 2:
|
||||
return False
|
||||
else:
|
||||
@ -231,11 +368,17 @@ def crypt_salt_is_valid(salt):
|
||||
|
||||
|
||||
class LdapHashUserPassword(object):
|
||||
"""Please see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html"""
|
||||
"""
|
||||
Class to deal with hashed password as defined at
|
||||
https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html
|
||||
"""
|
||||
|
||||
#: valide schemes that require a salt
|
||||
schemes_salt = {b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}", b"{CRYPT}"}
|
||||
#: valide sschemes that require no slat
|
||||
schemes_nosalt = {b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"}
|
||||
|
||||
#: map beetween scheme and hash function
|
||||
_schemes_to_hash = {
|
||||
b"{SMD5}": hashlib.md5,
|
||||
b"{MD5}": hashlib.md5,
|
||||
@ -249,6 +392,7 @@ class LdapHashUserPassword(object):
|
||||
b"{SHA512}": hashlib.sha512
|
||||
}
|
||||
|
||||
#: map between scheme and hash length
|
||||
_schemes_to_len = {
|
||||
b"{SMD5}": 16,
|
||||
b"{SSHA}": 20,
|
||||
@ -258,7 +402,10 @@ class LdapHashUserPassword(object):
|
||||
}
|
||||
|
||||
class BadScheme(ValueError):
|
||||
"""Error raised then the hash scheme is not in schemes_salt + schemes_nosalt"""
|
||||
"""
|
||||
Error raised then the hash scheme is not in
|
||||
:attr:`LdapHashUserPassword.schemes_salt` + :attr:`LdapHashUserPassword.schemes_nosalt`
|
||||
"""
|
||||
pass
|
||||
|
||||
class BadHash(ValueError):
|
||||
@ -266,14 +413,19 @@ class LdapHashUserPassword(object):
|
||||
pass
|
||||
|
||||
class BadSalt(ValueError):
|
||||
"""Error raised then with the scheme {CRYPT} the salt is invalid"""
|
||||
"""Error raised then, with the scheme ``{CRYPT}``, the salt is invalid"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _raise_bad_scheme(cls, scheme, valid, msg):
|
||||
"""
|
||||
Raise BadScheme error for `scheme`, possible valid scheme are
|
||||
in `valid`, the error message is `msg`
|
||||
Raise :attr:`BadScheme` error for ``scheme``, possible valid scheme are
|
||||
in ``valid``, the error message is ``msg``
|
||||
|
||||
:param bytes scheme: A bad scheme
|
||||
:param list valid: A list a valid scheme
|
||||
:param str msg: The error template message
|
||||
:raises LdapHashUserPassword.BadScheme: always
|
||||
"""
|
||||
valid_schemes = [s.decode() for s in valid]
|
||||
valid_schemes.sort()
|
||||
@ -281,7 +433,12 @@ class LdapHashUserPassword(object):
|
||||
|
||||
@classmethod
|
||||
def _test_scheme(cls, scheme):
|
||||
"""Test if a scheme is valide or raise BadScheme"""
|
||||
"""
|
||||
Test if a scheme is valide or raise BadScheme
|
||||
|
||||
:param bytes scheme: A scheme
|
||||
:raises BadScheme: if ``scheme`` is not a valid scheme
|
||||
"""
|
||||
if scheme not in cls.schemes_salt and scheme not in cls.schemes_nosalt:
|
||||
cls._raise_bad_scheme(
|
||||
scheme,
|
||||
@ -291,7 +448,12 @@ class LdapHashUserPassword(object):
|
||||
|
||||
@classmethod
|
||||
def _test_scheme_salt(cls, scheme):
|
||||
"""Test if the scheme need a salt or raise BadScheme"""
|
||||
"""
|
||||
Test if the scheme need a salt or raise BadScheme
|
||||
|
||||
:param bytes scheme: A scheme
|
||||
:raises BadScheme: if ``scheme` require no salt
|
||||
"""
|
||||
if scheme not in cls.schemes_salt:
|
||||
cls._raise_bad_scheme(
|
||||
scheme,
|
||||
@ -301,7 +463,12 @@ class LdapHashUserPassword(object):
|
||||
|
||||
@classmethod
|
||||
def _test_scheme_nosalt(cls, scheme):
|
||||
"""Test if the scheme need no salt or raise BadScheme"""
|
||||
"""
|
||||
Test if the scheme need no salt or raise BadScheme
|
||||
|
||||
:param bytes scheme: A scheme
|
||||
:raises BadScheme: if ``scheme` require a salt
|
||||
"""
|
||||
if scheme not in cls.schemes_nosalt:
|
||||
cls._raise_bad_scheme(
|
||||
scheme,
|
||||
@ -312,8 +479,15 @@ class LdapHashUserPassword(object):
|
||||
@classmethod
|
||||
def hash(cls, scheme, password, salt=None, charset="utf8"):
|
||||
"""
|
||||
Hash `password` with `scheme` using `salt`.
|
||||
This three variable beeing encoded in `charset`.
|
||||
Hash ``password`` with ``scheme`` using ``salt``.
|
||||
This three variable beeing encoded in ``charset``.
|
||||
|
||||
:param bytes scheme: A valid scheme
|
||||
:param bytes password: A byte string to hash using ``scheme``
|
||||
:param bytes salt: An optional salt to use if ``scheme`` requires any
|
||||
:param str charset: The encoding of ``scheme``, ``password`` and ``salt``
|
||||
:return: The hashed password encoded with ``charset``
|
||||
:rtype: bytes
|
||||
"""
|
||||
scheme = scheme.upper()
|
||||
cls._test_scheme(scheme)
|
||||
@ -339,7 +513,14 @@ class LdapHashUserPassword(object):
|
||||
|
||||
@classmethod
|
||||
def get_scheme(cls, hashed_passord):
|
||||
"""Return the scheme of `hashed_passord` or raise BadHash"""
|
||||
"""
|
||||
Return the scheme of ``hashed_passord`` or raise :attr:`BadHash`
|
||||
|
||||
:param bytes hashed_passord: A hashed password
|
||||
:return: The scheme used by the hashed password
|
||||
:rtype: bytes
|
||||
:raises BadHash: if no valid scheme is found within ``hashed_passord``
|
||||
"""
|
||||
if not hashed_passord[0] == b'{'[0] or b'}' not in hashed_passord:
|
||||
raise cls.BadHash("%r should start with the scheme enclosed with { }" % hashed_passord)
|
||||
scheme = hashed_passord.split(b'}', 1)[0]
|
||||
@ -348,7 +529,15 @@ class LdapHashUserPassword(object):
|
||||
|
||||
@classmethod
|
||||
def get_salt(cls, hashed_passord):
|
||||
"""Return the salt of `hashed_passord` possibly empty"""
|
||||
"""
|
||||
Return the salt of ``hashed_passord`` possibly empty
|
||||
|
||||
:param bytes hashed_passord: A hashed password
|
||||
:return: The salt used by the hashed password (empty if no salt is used)
|
||||
:rtype: bytes
|
||||
:raises BadHash: if no valid scheme is found within ``hashed_passord`` or if the
|
||||
hashed password is too short for the scheme found.
|
||||
"""
|
||||
scheme = cls.get_scheme(hashed_passord)
|
||||
cls._test_scheme(scheme)
|
||||
if scheme in cls.schemes_nosalt:
|
||||
@ -364,8 +553,20 @@ class LdapHashUserPassword(object):
|
||||
|
||||
def check_password(method, password, hashed_password, charset):
|
||||
"""
|
||||
Check that `password` match `hashed_password` using `method`,
|
||||
assuming the encoding is `charset`.
|
||||
Check that ``password`` match `hashed_password` using ``method``,
|
||||
assuming the encoding is ``charset``.
|
||||
|
||||
:param str method: on of ``"crypt"``, ``"ldap"``, ``"hex_md5"``, ``"hex_sha1"``,
|
||||
``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, ``"hex_sha512"``, ``"plain"``
|
||||
:param password: The user inputed password
|
||||
:type password: :obj:`str` or :obj:`unicode`
|
||||
:param hashed_password: The hashed password as stored in the database
|
||||
:type hashed_password: :obj:`str` or :obj:`unicode`
|
||||
:param str charset: The used char encoding (also used internally, so it must be valid for
|
||||
the charset used by ``password`` even if it is inputed as an :obj:`unicode`)
|
||||
:return: True if ``password`` match ``hashed_password`` using ``method``,
|
||||
``False`` otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
if not isinstance(password, six.binary_type):
|
||||
password = password.encode(charset)
|
||||
|
File diff suppressed because it is too large
Load Diff
225
docs/Makefile
Normal file
225
docs/Makefile
Normal file
@ -0,0 +1,225 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " epub3 to make an epub3"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
@echo " dummy to check syntax errors of document sources"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
.PHONY: singlehtml
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
.PHONY: pickle
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
.PHONY: json
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
.PHONY: htmlhelp
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
.PHONY: qthelp
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-cas-server.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-cas-server.qhc"
|
||||
|
||||
.PHONY: applehelp
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
.PHONY: devhelp
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-cas-server"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-cas-server"
|
||||
@echo "# devhelp"
|
||||
|
||||
.PHONY: epub
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
.PHONY: epub3
|
||||
epub3:
|
||||
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
||||
@echo
|
||||
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
||||
|
||||
.PHONY: latex
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
.PHONY: latexpdf
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: latexpdfja
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: text
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
.PHONY: man
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
.PHONY: texinfo
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
.PHONY: info
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
.PHONY: gettext
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
.PHONY: changes
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
.PHONY: linkcheck
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
.PHONY: doctest
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
.PHONY: xml
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
.PHONY: pseudoxml
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
|
||||
.PHONY: dummy
|
||||
dummy:
|
||||
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
||||
@echo
|
||||
@echo "Build finished. Dummy builder generates no files."
|
1
docs/README.rst
Normal file
1
docs/README.rst
Normal file
@ -0,0 +1 @@
|
||||
.. include:: ../README.rst
|
321
docs/_ext/djangodocs.py
Normal file
321
docs/_ext/djangodocs.py
Normal file
@ -0,0 +1,321 @@
|
||||
"""
|
||||
Sphinx plugins for Django documentation.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import directives
|
||||
from sphinx import addnodes
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
from sphinx.domains.std import Cmdoption
|
||||
from sphinx.util.compat import Directive
|
||||
from sphinx.util.console import bold
|
||||
from sphinx.util.nodes import set_source_info
|
||||
from sphinx.writers.html import SmartyPantsHTMLTranslator
|
||||
|
||||
# RE for option descriptions without a '--' prefix
|
||||
simple_option_desc_re = re.compile(
|
||||
r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)')
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_crossref_type(
|
||||
directivename="setting",
|
||||
rolename="setting",
|
||||
indextemplate="pair: %s; setting",
|
||||
)
|
||||
app.add_crossref_type(
|
||||
directivename="templatetag",
|
||||
rolename="ttag",
|
||||
indextemplate="pair: %s; template tag"
|
||||
)
|
||||
app.add_crossref_type(
|
||||
directivename="templatefilter",
|
||||
rolename="tfilter",
|
||||
indextemplate="pair: %s; template filter"
|
||||
)
|
||||
app.add_crossref_type(
|
||||
directivename="fieldlookup",
|
||||
rolename="lookup",
|
||||
indextemplate="pair: %s; field lookup type",
|
||||
)
|
||||
app.add_description_unit(
|
||||
directivename="django-admin",
|
||||
rolename="djadmin",
|
||||
indextemplate="pair: %s; django-admin command",
|
||||
parse_node=parse_django_admin_node,
|
||||
)
|
||||
app.add_directive('django-admin-option', Cmdoption)
|
||||
app.add_config_value('django_next_version', '0.0', True)
|
||||
app.add_directive('versionadded', VersionDirective)
|
||||
app.add_directive('versionchanged', VersionDirective)
|
||||
app.add_builder(DjangoStandaloneHTMLBuilder)
|
||||
|
||||
# register the snippet directive
|
||||
app.add_directive('snippet', SnippetWithFilename)
|
||||
# register a node for snippet directive so that the xml parser
|
||||
# knows how to handle the enter/exit parsing event
|
||||
app.add_node(snippet_with_filename,
|
||||
html=(visit_snippet, depart_snippet_literal),
|
||||
latex=(visit_snippet_latex, depart_snippet_latex),
|
||||
man=(visit_snippet_literal, depart_snippet_literal),
|
||||
text=(visit_snippet_literal, depart_snippet_literal),
|
||||
texinfo=(visit_snippet_literal, depart_snippet_literal))
|
||||
return {'parallel_read_safe': True}
|
||||
|
||||
|
||||
class snippet_with_filename(nodes.literal_block):
|
||||
"""
|
||||
Subclass the literal_block to override the visit/depart event handlers
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def visit_snippet_literal(self, node):
|
||||
"""
|
||||
default literal block handler
|
||||
"""
|
||||
self.visit_literal_block(node)
|
||||
|
||||
|
||||
def depart_snippet_literal(self, node):
|
||||
"""
|
||||
default literal block handler
|
||||
"""
|
||||
self.depart_literal_block(node)
|
||||
|
||||
|
||||
def visit_snippet(self, node):
|
||||
"""
|
||||
HTML document generator visit handler
|
||||
"""
|
||||
lang = self.highlightlang
|
||||
linenos = node.rawsource.count('\n') >= self.highlightlinenothreshold - 1
|
||||
fname = node['filename']
|
||||
highlight_args = node.get('highlight_args', {})
|
||||
if 'language' in node:
|
||||
# code-block directives
|
||||
lang = node['language']
|
||||
highlight_args['force'] = True
|
||||
if 'linenos' in node:
|
||||
linenos = node['linenos']
|
||||
|
||||
def warner(msg):
|
||||
self.builder.warn(msg, (self.builder.current_docname, node.line))
|
||||
|
||||
highlighted = self.highlighter.highlight_block(node.rawsource, lang,
|
||||
warn=warner,
|
||||
linenos=linenos,
|
||||
**highlight_args)
|
||||
starttag = self.starttag(node, 'div', suffix='',
|
||||
CLASS='highlight-%s snippet' % lang)
|
||||
self.body.append(starttag)
|
||||
self.body.append('<div class="snippet-filename">%s</div>\n''' % (fname,))
|
||||
self.body.append(highlighted)
|
||||
self.body.append('</div>\n')
|
||||
raise nodes.SkipNode
|
||||
|
||||
|
||||
def visit_snippet_latex(self, node):
|
||||
"""
|
||||
Latex document generator visit handler
|
||||
"""
|
||||
code = node.rawsource.rstrip('\n')
|
||||
|
||||
lang = self.hlsettingstack[-1][0]
|
||||
linenos = code.count('\n') >= self.hlsettingstack[-1][1] - 1
|
||||
fname = node['filename']
|
||||
highlight_args = node.get('highlight_args', {})
|
||||
if 'language' in node:
|
||||
# code-block directives
|
||||
lang = node['language']
|
||||
highlight_args['force'] = True
|
||||
if 'linenos' in node:
|
||||
linenos = node['linenos']
|
||||
|
||||
def warner(msg):
|
||||
self.builder.warn(msg, (self.curfilestack[-1], node.line))
|
||||
|
||||
hlcode = self.highlighter.highlight_block(code, lang, warn=warner,
|
||||
linenos=linenos,
|
||||
**highlight_args)
|
||||
|
||||
self.body.append(
|
||||
'\n{\\colorbox[rgb]{0.9,0.9,0.9}'
|
||||
'{\\makebox[\\textwidth][l]'
|
||||
'{\\small\\texttt{%s}}}}\n' % (
|
||||
# Some filenames have '_', which is special in latex.
|
||||
fname.replace('_', r'\_'),
|
||||
)
|
||||
)
|
||||
|
||||
if self.table:
|
||||
hlcode = hlcode.replace('\\begin{Verbatim}',
|
||||
'\\begin{OriginalVerbatim}')
|
||||
self.table.has_problematic = True
|
||||
self.table.has_verbatim = True
|
||||
|
||||
hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim}
|
||||
hlcode = hlcode.rstrip() + '\n'
|
||||
self.body.append('\n' + hlcode + '\\end{%sVerbatim}\n' %
|
||||
(self.table and 'Original' or ''))
|
||||
|
||||
# Prevent rawsource from appearing in output a second time.
|
||||
raise nodes.SkipNode
|
||||
|
||||
|
||||
def depart_snippet_latex(self, node):
|
||||
"""
|
||||
Latex document generator depart handler.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class SnippetWithFilename(Directive):
|
||||
"""
|
||||
The 'snippet' directive that allows to add the filename (optional)
|
||||
of a code snippet in the document. This is modeled after CodeBlock.
|
||||
"""
|
||||
has_content = True
|
||||
optional_arguments = 1
|
||||
option_spec = {'filename': directives.unchanged_required}
|
||||
|
||||
def run(self):
|
||||
code = '\n'.join(self.content)
|
||||
|
||||
literal = snippet_with_filename(code, code)
|
||||
if self.arguments:
|
||||
literal['language'] = self.arguments[0]
|
||||
literal['filename'] = self.options['filename']
|
||||
set_source_info(self, literal)
|
||||
return [literal]
|
||||
|
||||
|
||||
class VersionDirective(Directive):
|
||||
has_content = True
|
||||
required_arguments = 1
|
||||
optional_arguments = 1
|
||||
final_argument_whitespace = True
|
||||
option_spec = {}
|
||||
|
||||
def run(self):
|
||||
if len(self.arguments) > 1:
|
||||
msg = """Only one argument accepted for directive '{directive_name}::'.
|
||||
Comments should be provided as content,
|
||||
not as an extra argument.""".format(directive_name=self.name)
|
||||
raise self.error(msg)
|
||||
|
||||
env = self.state.document.settings.env
|
||||
ret = []
|
||||
node = addnodes.versionmodified()
|
||||
ret.append(node)
|
||||
|
||||
if self.arguments[0] == env.config.django_next_version:
|
||||
node['version'] = "Development version"
|
||||
else:
|
||||
node['version'] = self.arguments[0]
|
||||
|
||||
node['type'] = self.name
|
||||
if self.content:
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
env.note_versionchange(node['type'], node['version'], node, self.lineno)
|
||||
return ret
|
||||
|
||||
|
||||
class DjangoHTMLTranslator(SmartyPantsHTMLTranslator):
|
||||
"""
|
||||
Django-specific reST to HTML tweaks.
|
||||
"""
|
||||
|
||||
# Don't use border=1, which docutils does by default.
|
||||
def visit_table(self, node):
|
||||
self.context.append(self.compact_p)
|
||||
self.compact_p = True
|
||||
self._table_row_index = 0 # Needed by Sphinx
|
||||
self.body.append(self.starttag(node, 'table', CLASS='docutils'))
|
||||
|
||||
def depart_table(self, node):
|
||||
self.compact_p = self.context.pop()
|
||||
self.body.append('</table>\n')
|
||||
|
||||
def visit_desc_parameterlist(self, node):
|
||||
self.body.append('(') # by default sphinx puts <big> around the "("
|
||||
self.first_param = 1
|
||||
self.optional_param_level = 0
|
||||
self.param_separator = node.child_text_separator
|
||||
self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
|
||||
for c in node.children])
|
||||
|
||||
def depart_desc_parameterlist(self, node):
|
||||
self.body.append(')')
|
||||
|
||||
#
|
||||
# Turn the "new in version" stuff (versionadded/versionchanged) into a
|
||||
# better callout -- the Sphinx default is just a little span,
|
||||
# which is a bit less obvious that I'd like.
|
||||
#
|
||||
# FIXME: these messages are all hardcoded in English. We need to change
|
||||
# that to accommodate other language docs, but I can't work out how to make
|
||||
# that work.
|
||||
#
|
||||
version_text = {
|
||||
'versionchanged': 'Changed in Django %s',
|
||||
'versionadded': 'New in Django %s',
|
||||
}
|
||||
|
||||
def visit_versionmodified(self, node):
|
||||
self.body.append(
|
||||
self.starttag(node, 'div', CLASS=node['type'])
|
||||
)
|
||||
version_text = self.version_text.get(node['type'])
|
||||
if version_text:
|
||||
title = "%s%s" % (
|
||||
version_text % node['version'],
|
||||
":" if len(node) else "."
|
||||
)
|
||||
self.body.append('<span class="title">%s</span> ' % title)
|
||||
|
||||
def depart_versionmodified(self, node):
|
||||
self.body.append("</div>\n")
|
||||
|
||||
# Give each section a unique ID -- nice for custom CSS hooks
|
||||
def visit_section(self, node):
|
||||
old_ids = node.get('ids', [])
|
||||
node['ids'] = ['s-' + i for i in old_ids]
|
||||
node['ids'].extend(old_ids)
|
||||
SmartyPantsHTMLTranslator.visit_section(self, node)
|
||||
node['ids'] = old_ids
|
||||
|
||||
|
||||
def parse_django_admin_node(env, sig, signode):
|
||||
command = sig.split(' ')[0]
|
||||
env.ref_context['std:program'] = command
|
||||
title = "django-admin %s" % sig
|
||||
signode += addnodes.desc_name(title, title)
|
||||
return command
|
||||
|
||||
|
||||
class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder):
|
||||
"""
|
||||
Subclass to add some extra things we need.
|
||||
"""
|
||||
|
||||
name = 'djangohtml'
|
||||
|
||||
def finish(self):
|
||||
super(DjangoStandaloneHTMLBuilder, self).finish()
|
||||
self.info(bold("writing templatebuiltins.js..."))
|
||||
xrefs = self.env.domaindata["std"]["objects"]
|
||||
templatebuiltins = {
|
||||
"ttags": [n for ((t, n), (l, a)) in xrefs.items()
|
||||
if t == "templatetag" and l == "ref/templates/builtins"],
|
||||
"tfilters": [n for ((t, n), (l, a)) in xrefs.items()
|
||||
if t == "templatefilter" and l == "ref/templates/builtins"],
|
||||
}
|
||||
outfilename = os.path.join(self.outdir, "templatebuiltins.js")
|
||||
with open(outfilename, 'w') as fp:
|
||||
fp.write('var django_template_builtins = ')
|
||||
json.dump(templatebuiltins, fp)
|
||||
fp.write(';\n')
|
369
docs/conf.py
Normal file
369
docs/conf.py
Normal file
@ -0,0 +1,369 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# django-cas-server documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Jul 5 12:11:50 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('.'))
|
||||
sys.path.append(os.path.abspath('..'))
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext")))
|
||||
|
||||
import setup as mysetup
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'cas_server.tests.settings'
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'djangodocs',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'django-cas-server'
|
||||
copyright = u'2016, Valentin Samir'
|
||||
author = u'Valentin Samir'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = mysetup.VERSION
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#
|
||||
# today = ''
|
||||
#
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
#html_theme = 'alabaster'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents.
|
||||
# "<project> v<release> documentation" by default.
|
||||
#
|
||||
# html_title = u'django-cas-server v5.0'
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#
|
||||
# html_extra_path = []
|
||||
|
||||
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||
# bottom, using the given strftime format.
|
||||
# The empty string is equivalent to '%b %d, %Y'.
|
||||
#
|
||||
# html_last_updated_fmt = None
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
|
||||
#
|
||||
# html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# 'ja' uses this config value.
|
||||
# 'zh' user can custom change `jieba` dictionary path.
|
||||
#
|
||||
# html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#
|
||||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-cas-serverdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'django-cas-server.tex', u'django-cas-server Documentation',
|
||||
u'Valentin Samir', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'django-cas-server', u'django-cas-server Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'django-cas-server', u'django-cas-server Documentation',
|
||||
author, 'django-cas-server', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
"python": ('https://docs.python.org/', None),
|
||||
"django": ('https://docs.djangoproject.com/en/1.9/', 'django.inv'),
|
||||
}
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
|
||||
locale_dirs = ['../test_venv/lib/python2.7/site-packages/django/conf/locale/']
|
||||
|
||||
|
||||
def _download_django_inv():
|
||||
import requests
|
||||
with open(_download_django_inv.path, 'w') as f:
|
||||
r = requests.get("https://docs.djangoproject.com/en/1.9/_objects")
|
||||
f.write(r.content)
|
||||
_download_django_inv.path = os.path.abspath(os.path.join(os.path.dirname(__file__), "django.inv"))
|
||||
|
||||
if not os.path.isfile(_download_django_inv.path):
|
||||
_download_django_inv()
|
23
docs/index.rst
Normal file
23
docs/index.rst
Normal file
@ -0,0 +1,23 @@
|
||||
.. django-cas-server documentation master file, created by
|
||||
sphinx-quickstart on Tue Jul 5 12:11:50 2016.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
django-cas-server documentation
|
||||
===============================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
README
|
||||
package/cas_server
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
.. * :ref:`modindex`
|
||||
.. * :ref:`search`
|
||||
|
281
docs/make.bat
Normal file
281
docs/make.bat
Normal file
@ -0,0 +1,281 @@
|
||||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. epub3 to make an epub3
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. xml to make Docutils-native XML files
|
||||
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
echo. coverage to run coverage check of the documentation if enabled
|
||||
echo. dummy to check syntax errors of document sources
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
REM Check if sphinx-build is available and fallback to Python version if any
|
||||
%SPHINXBUILD% 1>NUL 2>NUL
|
||||
if errorlevel 9009 goto sphinx_python
|
||||
goto sphinx_ok
|
||||
|
||||
:sphinx_python
|
||||
|
||||
set SPHINXBUILD=python -m sphinx.__init__
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:sphinx_ok
|
||||
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-cas-server.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-cas-server.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub3" (
|
||||
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdf" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf
|
||||
cd %~dp0
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdfja" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf-ja
|
||||
cd %~dp0
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "coverage" (
|
||||
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of coverage in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/coverage/python.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "xml" (
|
||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pseudoxml" (
|
||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dummy" (
|
||||
%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. Dummy builder generates no files.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
7
docs/package/cas_server.admin.rst
Normal file
7
docs/package/cas_server.admin.rst
Normal file
@ -0,0 +1,7 @@
|
||||
cas_server.admin module
|
||||
=======================
|
||||
|
||||
.. automodule:: cas_server.admin
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
6
docs/package/cas_server.apps.rst
Normal file
6
docs/package/cas_server.apps.rst
Normal file
@ -0,0 +1,6 @@
|
||||
cas_server.apps module
|
||||
======================
|
||||
|
||||
.. automodule:: cas_server.apps
|
||||
:members:
|
||||
:undoc-members:
|
7
docs/package/cas_server.auth.rst
Normal file
7
docs/package/cas_server.auth.rst
Normal file
@ -0,0 +1,7 @@
|
||||
cas_server.auth module
|
||||
======================
|
||||
|
||||
.. automodule:: cas_server.auth
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
7
docs/package/cas_server.cas.rst
Normal file
7
docs/package/cas_server.cas.rst
Normal file
@ -0,0 +1,7 @@
|
||||
cas_server.cas module
|
||||
=====================
|
||||
|
||||
.. automodule:: cas_server.cas
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
7
docs/package/cas_server.default_settings.rst
Normal file
7
docs/package/cas_server.default_settings.rst
Normal file
@ -0,0 +1,7 @@
|
||||
cas_server.default_settings module
|
||||
==================================
|
||||
|
||||
.. automodule:: cas_server.default_settings
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
7
docs/package/cas_server.federate.rst
Normal file
7
docs/package/cas_server.federate.rst
Normal file
@ -0,0 +1,7 @@
|
||||
cas_server.federate module
|
||||
==========================
|
||||
|
||||
.. automodule:: cas_server.federate
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
5
docs/package/cas_server.forms.rst
Normal file
5
docs/package/cas_server.forms.rst
Normal file
@ -0,0 +1,5 @@
|
||||
cas_server.forms module
|
||||
=======================
|
||||
|
||||
.. automodule:: cas_server.forms
|
||||
:members:
|
6
docs/package/cas_server.models.rst
Normal file
6
docs/package/cas_server.models.rst
Normal file
@ -0,0 +1,6 @@
|
||||
cas_server.models module
|
||||
========================
|
||||
|
||||
.. automodule:: cas_server.models
|
||||
:members:
|
||||
:undoc-members:
|
27
docs/package/cas_server.rst
Normal file
27
docs/package/cas_server.rst
Normal file
@ -0,0 +1,27 @@
|
||||
cas_server package
|
||||
==================
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
cas_server.admin
|
||||
cas_server.apps
|
||||
cas_server.auth
|
||||
cas_server.cas
|
||||
cas_server.default_settings
|
||||
cas_server.federate
|
||||
cas_server.forms
|
||||
cas_server.models
|
||||
cas_server.urls
|
||||
cas_server.utils
|
||||
cas_server.views
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: cas_server
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
7
docs/package/cas_server.utils.rst
Normal file
7
docs/package/cas_server.utils.rst
Normal file
@ -0,0 +1,7 @@
|
||||
cas_server.utils module
|
||||
=======================
|
||||
|
||||
.. automodule:: cas_server.utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
7
docs/package/cas_server.views.rst
Normal file
7
docs/package/cas_server.views.rst
Normal file
@ -0,0 +1,7 @@
|
||||
cas_server.views module
|
||||
=======================
|
||||
|
||||
.. automodule:: cas_server.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
7
docs/package/modules.rst
Normal file
7
docs/package/modules.rst
Normal file
@ -0,0 +1,7 @@
|
||||
cas_server
|
||||
==========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
cas_server
|
@ -6,7 +6,5 @@ pytest-pythonpath>=0.3
|
||||
pytest-cov>=2.2.1
|
||||
requests>=2.4
|
||||
requests_futures>=0.9.5
|
||||
django-picklefield>=0.3.1
|
||||
django-bootstrap3>=5.4
|
||||
lxml>=3.4
|
||||
six>=1
|
||||
|
@ -2,7 +2,5 @@ Django >= 1.8,<1.10
|
||||
setuptools>=5.5
|
||||
requests>=2.4
|
||||
requests_futures>=0.9.5
|
||||
django-picklefield>=0.3.1
|
||||
django-bootstrap3>=5.4
|
||||
lxml>=3.4
|
||||
six>=1
|
||||
|
11
setup.cfg
11
setup.cfg
@ -1,2 +1,13 @@
|
||||
[metadata]
|
||||
description-file = README.rst
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
tag_svn_revision = 0
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
53
setup.py
53
setup.py
@ -2,36 +2,18 @@ import os
|
||||
import pkg_resources
|
||||
from setuptools import setup
|
||||
|
||||
VERSION = '0.6.0'
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
|
||||
README = readme.read()
|
||||
|
||||
# allow setup.py to be run from any path
|
||||
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
||||
if __name__ == '__main__':
|
||||
# allow setup.py to be run from any path
|
||||
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
||||
|
||||
|
||||
# if we have Django 1.8 available, use last version of django-boostrap3
|
||||
try:
|
||||
pkg_resources.require('Django >= 1.8')
|
||||
django_bootstrap3 = 'django-bootstrap3 >= 5.4'
|
||||
django = 'Django >= 1.8,<1.10'
|
||||
except pkg_resources.VersionConflict:
|
||||
# Else if we have django 1.7, we need django-boostrap3 < 7.0.0
|
||||
try:
|
||||
pkg_resources.require('Django >= 1.7')
|
||||
django_bootstrap3 = 'django-bootstrap3 >= 5.4,<7.0.0'
|
||||
django = 'Django >= 1.7,<1.8'
|
||||
except (pkg_resources.VersionConflict, pkg_resources.DistributionNotFound):
|
||||
# Else we need to install Django, assume version will be >= 1.8
|
||||
django_bootstrap3 = 'django-bootstrap3 >= 5.4'
|
||||
django = 'Django >= 1.8,<1.10'
|
||||
# No version of django installed, assume version will be >= 1.8
|
||||
except pkg_resources.DistributionNotFound:
|
||||
django_bootstrap3 = 'django-bootstrap3 >= 5.4'
|
||||
django = 'Django >= 1.8,<1.10'
|
||||
|
||||
setup(
|
||||
setup(
|
||||
name='django-cas-server',
|
||||
version='0.6.0',
|
||||
version=VERSION,
|
||||
packages=[
|
||||
'cas_server', 'cas_server.migrations',
|
||||
'cas_server.management', 'cas_server.management.commands',
|
||||
@ -48,15 +30,25 @@ setup(
|
||||
author_email='valentin.samir@crans.org',
|
||||
classifiers=[
|
||||
'Environment :: Web Environment',
|
||||
'evelopment Status :: 5 - Production/Stable',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.7',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||
'Topic :: System :: Systems Administration :: Authentication/Directory'
|
||||
],
|
||||
package_data={
|
||||
'cas_server': [
|
||||
@ -67,11 +59,12 @@ setup(
|
||||
},
|
||||
keywords=['django', 'cas', 'cas3', 'server', 'sso', 'single sign-on', 'authentication', 'auth'],
|
||||
install_requires=[
|
||||
django, 'requests >= 2.4', 'requests_futures >= 0.9.5',
|
||||
'django-picklefield >= 0.3.1', django_bootstrap3, 'lxml >= 3.4',
|
||||
'six >= 1'
|
||||
'Django >= 1.7,<1.10', 'requests >= 2.4', 'requests_futures >= 0.9.5',
|
||||
'lxml >= 3.4', 'six >= 1'
|
||||
],
|
||||
url="https://github.com/nitmir/django-cas-server",
|
||||
download_url="https://github.com/nitmir/django-cas-server/releases",
|
||||
zip_safe=False
|
||||
)
|
||||
zip_safe=False,
|
||||
setup_requires=['pytest-runner'],
|
||||
tests_require=['pytest', 'pytest-django', 'pytest-pythonpath'],
|
||||
)
|
||||
|
15
tox.ini
15
tox.ini
@ -8,6 +8,8 @@ envlist=
|
||||
py34-django17,
|
||||
py34-django18,
|
||||
py34-django19,
|
||||
py35-django18,
|
||||
py35-django19,
|
||||
|
||||
[flake8]
|
||||
max-line-length=100
|
||||
@ -69,6 +71,19 @@ deps =
|
||||
Django>=1.9,<1.10
|
||||
{[base]deps}
|
||||
|
||||
[testenv:py35-django18]
|
||||
basepython=python3.5
|
||||
deps =
|
||||
Django>=1.8,<1.9
|
||||
{[base]deps}
|
||||
|
||||
[testenv:py35-django19]
|
||||
basepython=python3.5
|
||||
deps =
|
||||
Django>=1.9,<1.10
|
||||
{[base]deps}
|
||||
|
||||
|
||||
[testenv:flake8]
|
||||
basepython=python
|
||||
deps=flake8
|
||||
|
Loading…
Reference in New Issue
Block a user