commit
f837853043
4
.gitignore
vendored
4
.gitignore
vendored
@ -4,12 +4,13 @@
|
|||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
build/
|
build/
|
||||||
bootstrap3
|
|
||||||
cas/
|
cas/
|
||||||
dist/
|
dist/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
manage.py
|
manage.py
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
docs/_build/
|
||||||
|
docs/django.inv
|
||||||
|
|
||||||
.tox
|
.tox
|
||||||
test_venv
|
test_venv
|
||||||
@ -17,3 +18,4 @@ test_venv
|
|||||||
htmlcov/
|
htmlcov/
|
||||||
tox_logs/
|
tox_logs/
|
||||||
.cache/
|
.cache/
|
||||||
|
.eggs/
|
||||||
|
37
.travis.yml
37
.travis.yml
@ -1,17 +1,28 @@
|
|||||||
language: python
|
language: python
|
||||||
python:
|
matrix:
|
||||||
- "2.7"
|
include:
|
||||||
env:
|
- python: "2.7"
|
||||||
matrix:
|
env: TOX_ENV=coverage
|
||||||
- TOX_ENV=coverage
|
- python: "2.7"
|
||||||
- TOX_ENV=flake8
|
env: TOX_ENV=flake8
|
||||||
- TOX_ENV=check_rst
|
- python: "2.7"
|
||||||
- TOX_ENV=py27-django17
|
env: TOX_ENV=check_rst
|
||||||
- TOX_ENV=py27-django18
|
- python: "2.7"
|
||||||
- TOX_ENV=py27-django19
|
env: TOX_ENV=py27-django17
|
||||||
- TOX_ENV=py34-django17
|
- python: "2.7"
|
||||||
- TOX_ENV=py34-django18
|
env: TOX_ENV=py27-django18
|
||||||
- TOX_ENV=py34-django19
|
- 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:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.cache/pip/http/
|
- $HOME/.cache/pip/http/
|
||||||
|
14
MANIFEST.in
14
MANIFEST.in
@ -1,7 +1,21 @@
|
|||||||
include tox.ini
|
include tox.ini
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include README.rst
|
include README.rst
|
||||||
|
include .coveragerc
|
||||||
|
include Makefile
|
||||||
|
include pytest.ini
|
||||||
|
include requirements-dev.txt
|
||||||
|
include requirements.txt
|
||||||
prune .tox
|
prune .tox
|
||||||
recursive-include cas_server/static *
|
recursive-include cas_server/static *
|
||||||
recursive-include cas_server/templates *
|
recursive-include cas_server/templates *
|
||||||
recursive-include cas_server/locale *
|
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`
|
VERSION=`python setup.py -V`
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@ -24,10 +24,14 @@ clean_coverage:
|
|||||||
rm -rf coverage.xml .coverage htmlcov
|
rm -rf coverage.xml .coverage htmlcov
|
||||||
clean_tild_backup:
|
clean_tild_backup:
|
||||||
find ./ -name '*~' -delete
|
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: 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:
|
dist:
|
||||||
python setup.py sdist
|
python setup.py sdist
|
||||||
@ -40,7 +44,7 @@ test_venv/cas/manage.py: test_venv
|
|||||||
mkdir -p test_venv/cas
|
mkdir -p test_venv/cas
|
||||||
test_venv/bin/django-admin startproject cas test_venv/cas
|
test_venv/bin/django-admin startproject cas test_venv/cas
|
||||||
ln -s ../../cas_server test_venv/cas/cas_server
|
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/'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/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
|
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
|
python setup.py check --restructuredtext --stric
|
||||||
test_venv/bin/py.test --cov=cas_server --cov-report html
|
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
|
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
|
CAS Server
|
||||||
##########
|
##########
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/nitmir/django-cas-server.svg?branch=master
|
|travis| |version| |lisence| |codacy| |coverage|
|
||||||
: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
|
|
||||||
|
|
||||||
CAS Server is a Django application implementing the `CAS Protocol 3.0 Specification
|
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>`_.
|
<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
|
By default, the authentication process use django internal users but you can easily
|
||||||
use any sources (see auth classes in the auth.py file)
|
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
|
.. contents:: Table of Contents
|
||||||
|
|
||||||
Features
|
Features
|
||||||
@ -52,8 +32,6 @@ Dependencies
|
|||||||
* Django >= 1.7 < 1.10
|
* Django >= 1.7 < 1.10
|
||||||
* requests >= 2.4
|
* requests >= 2.4
|
||||||
* requests_futures >= 0.9.5
|
* 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
|
* lxml >= 3.4
|
||||||
* six >= 1
|
* six >= 1
|
||||||
|
|
||||||
@ -68,7 +46,7 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
|
|||||||
|
|
||||||
On debian like systems::
|
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
|
On debian jessie, you can use the version of python-django available in the
|
||||||
`backports <https://backports.debian.org/Instructions/>`_.
|
`backports <https://backports.debian.org/Instructions/>`_.
|
||||||
@ -118,7 +96,6 @@ Quick start
|
|||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
...
|
...
|
||||||
'bootstrap3',
|
|
||||||
'cas_server',
|
'cas_server',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -186,6 +163,17 @@ Template settings
|
|||||||
|
|
||||||
* ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default
|
* ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default
|
||||||
templates. Set it to ``False`` to disable it.
|
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
|
* ``CAS_LOGIN_TEMPLATE``: Path to the template showed on ``/login`` then the user
|
||||||
is not autenticated. The default is ``"cas_server/login.html"``.
|
is not autenticated. The default is ``"cas_server/login.html"``.
|
||||||
@ -489,3 +477,20 @@ You could for example do as bellow :
|
|||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate
|
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
|
# (c) 2015-2016 Valentin Samir
|
||||||
"""A django CAS server application"""
|
"""A django CAS server application"""
|
||||||
|
#: path the the application configuration class
|
||||||
default_app_config = 'cas_server.apps.CasAppConfig'
|
default_app_config = 'cas_server.apps.CasAppConfig'
|
||||||
|
@ -15,86 +15,155 @@ from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterA
|
|||||||
from .models import FederatedIendityProvider
|
from .models import FederatedIendityProvider
|
||||||
from .forms import TicketForm
|
from .forms import TicketForm
|
||||||
|
|
||||||
TICKETS_READONLY_FIELDS = ('validate', 'service', 'service_pattern',
|
|
||||||
'creation', 'renew', 'single_log_out', 'value')
|
class BaseInlines(admin.TabularInline):
|
||||||
TICKETS_FIELDS = ('validate', 'service', 'service_pattern',
|
"""
|
||||||
'creation', 'renew', 'single_log_out')
|
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):
|
class UserAdminInlines(BaseInlines):
|
||||||
"""`ServiceTicket` in admin interface"""
|
"""
|
||||||
|
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
|
model = ServiceTicket
|
||||||
extra = 0
|
|
||||||
form = TicketForm
|
|
||||||
readonly_fields = TICKETS_READONLY_FIELDS
|
|
||||||
fields = TICKETS_FIELDS
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyTicketInline(admin.TabularInline):
|
class ProxyTicketInline(UserAdminInlines):
|
||||||
"""`ProxyTicket` in admin interface"""
|
"""
|
||||||
|
Bases: :class:`UserAdminInlines`
|
||||||
|
|
||||||
|
:class:`ProxyTicket<cas_server.models.ProxyTicket>` in admin interface
|
||||||
|
"""
|
||||||
|
#: The model which the inline is using.
|
||||||
model = ProxyTicket
|
model = ProxyTicket
|
||||||
extra = 0
|
|
||||||
form = TicketForm
|
|
||||||
readonly_fields = TICKETS_READONLY_FIELDS
|
|
||||||
fields = TICKETS_FIELDS
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyGrantingInline(admin.TabularInline):
|
class ProxyGrantingInline(UserAdminInlines):
|
||||||
"""`ProxyGrantingTicket` in admin interface"""
|
"""
|
||||||
|
Bases: :class:`UserAdminInlines`
|
||||||
|
|
||||||
|
:class:`ProxyGrantingTicket<cas_server.models.ProxyGrantingTicket>` in admin interface
|
||||||
|
"""
|
||||||
|
#: The model which the inline is using.
|
||||||
model = ProxyGrantingTicket
|
model = ProxyGrantingTicket
|
||||||
extra = 0
|
|
||||||
form = TicketForm
|
|
||||||
readonly_fields = TICKETS_READONLY_FIELDS
|
|
||||||
fields = TICKETS_FIELDS[1:]
|
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(admin.ModelAdmin):
|
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)
|
inlines = (ServiceTicketInline, ProxyTicketInline, ProxyGrantingInline)
|
||||||
|
#: Fields to display on a object that are read only (not editable).
|
||||||
readonly_fields = ('username', 'date', "session_key")
|
readonly_fields = ('username', 'date', "session_key")
|
||||||
|
#: Fields to display on a object.
|
||||||
fields = ('username', 'date', "session_key")
|
fields = ('username', 'date', "session_key")
|
||||||
|
#: Fields to display on the list of class:`UserAdmin` objects.
|
||||||
list_display = ('username', 'date', "session_key")
|
list_display = ('username', 'date', "session_key")
|
||||||
|
|
||||||
|
|
||||||
class UsernamesInline(admin.TabularInline):
|
class UsernamesInline(BaseInlines):
|
||||||
"""`Username` in admin interface"""
|
"""
|
||||||
|
Bases: :class:`BaseInlines`
|
||||||
|
|
||||||
|
:class:`Username<cas_server.models.Username>` in admin interface
|
||||||
|
"""
|
||||||
|
#: The model which the inline is using.
|
||||||
model = Username
|
model = Username
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ReplaceAttributNameInline(admin.TabularInline):
|
class ReplaceAttributNameInline(BaseInlines):
|
||||||
"""`ReplaceAttributName` in admin interface"""
|
"""
|
||||||
|
Bases: :class:`BaseInlines`
|
||||||
|
|
||||||
|
:class:`ReplaceAttributName<cas_server.models.ReplaceAttributName>` in admin interface
|
||||||
|
"""
|
||||||
|
#: The model which the inline is using.
|
||||||
model = ReplaceAttributName
|
model = ReplaceAttributName
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ReplaceAttributValueInline(admin.TabularInline):
|
class ReplaceAttributValueInline(BaseInlines):
|
||||||
"""`ReplaceAttributValue` in admin interface"""
|
"""
|
||||||
|
Bases: :class:`BaseInlines`
|
||||||
|
|
||||||
|
:class:`ReplaceAttributValue<cas_server.models.ReplaceAttributValue>` in admin interface
|
||||||
|
"""
|
||||||
|
#: The model which the inline is using.
|
||||||
model = ReplaceAttributValue
|
model = ReplaceAttributValue
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class FilterAttributValueInline(admin.TabularInline):
|
class FilterAttributValueInline(BaseInlines):
|
||||||
"""`FilterAttributValue` in admin interface"""
|
"""
|
||||||
|
Bases: :class:`BaseInlines`
|
||||||
|
|
||||||
|
:class:`FilterAttributValue<cas_server.models.FilterAttributValue>` in admin interface
|
||||||
|
"""
|
||||||
|
#: The model which the inline is using.
|
||||||
model = FilterAttributValue
|
model = FilterAttributValue
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ServicePatternAdmin(admin.ModelAdmin):
|
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 = (
|
inlines = (
|
||||||
UsernamesInline,
|
UsernamesInline,
|
||||||
ReplaceAttributNameInline,
|
ReplaceAttributNameInline,
|
||||||
ReplaceAttributValueInline,
|
ReplaceAttributValueInline,
|
||||||
FilterAttributValueInline
|
FilterAttributValueInline
|
||||||
)
|
)
|
||||||
|
#: Fields to display on the list of class:`ServicePatternAdmin` objects.
|
||||||
list_display = ('pos', 'name', 'pattern', 'proxy',
|
list_display = ('pos', 'name', 'pattern', 'proxy',
|
||||||
'single_log_out', 'proxy_callback', 'restrict_users')
|
'single_log_out', 'proxy_callback', 'restrict_users')
|
||||||
|
|
||||||
|
|
||||||
class FederatedIendityProviderAdmin(admin.ModelAdmin):
|
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 = ('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')
|
list_display = ('verbose_name', 'suffix', 'display')
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,12 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class CasAppConfig(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'
|
name = 'cas_server'
|
||||||
|
#: Human-readable name for the application.
|
||||||
verbose_name = _('Central Authentication Service')
|
verbose_name = _('Central Authentication Service')
|
||||||
|
@ -26,55 +26,112 @@ from .models import FederatedUser
|
|||||||
|
|
||||||
|
|
||||||
class AuthUser(object):
|
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):
|
def __init__(self, username):
|
||||||
self.username = username
|
self.username = username
|
||||||
|
|
||||||
def test_password(self, password):
|
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()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def attributs(self):
|
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()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class DummyAuthUser(AuthUser): # pragma: no cover
|
class DummyAuthUser(AuthUser): # pragma: no cover
|
||||||
"""A Dummy authentication class"""
|
"""
|
||||||
|
A Dummy authentication class. Authentication always fails
|
||||||
|
|
||||||
def __init__(self, username):
|
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||||
super(DummyAuthUser, self).__init__(username)
|
class attribute. There is no valid value for this attribute here.
|
||||||
|
"""
|
||||||
|
|
||||||
def test_password(self, password):
|
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
|
return False
|
||||||
|
|
||||||
def attributs(self):
|
def attributs(self):
|
||||||
"""return a dict of user attributes"""
|
"""
|
||||||
|
The user attributes.
|
||||||
|
|
||||||
|
:return: en empty :class:`dict`.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class TestAuthUser(AuthUser):
|
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):
|
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||||
super(TestAuthUser, self).__init__(username)
|
class attribute. The uniq valid value is ``settings.CAS_TEST_USER``.
|
||||||
|
"""
|
||||||
|
|
||||||
def test_password(self, password):
|
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
|
return self.username == settings.CAS_TEST_USER and password == settings.CAS_TEST_PASSWORD
|
||||||
|
|
||||||
def attributs(self):
|
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
|
return settings.CAS_TEST_ATTRIBUTES
|
||||||
|
else: # pragma: no cover (should not happen)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class MysqlAuthUser(AuthUser): # pragma: no cover
|
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
|
user = None
|
||||||
|
|
||||||
def __init__(self, username):
|
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 = {
|
mysql_config = {
|
||||||
"user": settings.CAS_SQL_USERNAME,
|
"user": settings.CAS_SQL_USERNAME,
|
||||||
"passwd": settings.CAS_SQL_PASSWORD,
|
"passwd": settings.CAS_SQL_PASSWORD,
|
||||||
@ -94,7 +151,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
|
|||||||
super(MysqlAuthUser, self).__init__(username)
|
super(MysqlAuthUser, self).__init__(username)
|
||||||
|
|
||||||
def test_password(self, password):
|
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:
|
if self.user:
|
||||||
return check_password(
|
return check_password(
|
||||||
settings.CAS_SQL_PASSWORD_CHECK,
|
settings.CAS_SQL_PASSWORD_CHECK,
|
||||||
@ -106,7 +170,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def attributs(self):
|
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:
|
if self.user:
|
||||||
return self.user
|
return self.user
|
||||||
else:
|
else:
|
||||||
@ -114,7 +185,14 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
class DjangoAuthUser(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
|
user = None
|
||||||
|
|
||||||
def __init__(self, username):
|
def __init__(self, username):
|
||||||
@ -126,14 +204,27 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
|
|||||||
super(DjangoAuthUser, self).__init__(username)
|
super(DjangoAuthUser, self).__init__(username)
|
||||||
|
|
||||||
def test_password(self, password):
|
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:
|
if self.user:
|
||||||
return self.user.check_password(password)
|
return self.user.check_password(password)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def attributs(self):
|
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:
|
if self.user:
|
||||||
attr = {}
|
attr = {}
|
||||||
for field in self.user._meta.fields:
|
for field in self.user._meta.fields:
|
||||||
@ -144,7 +235,16 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
class CASFederateAuth(AuthUser):
|
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
|
user = None
|
||||||
|
|
||||||
def __init__(self, username):
|
def __init__(self, username):
|
||||||
@ -157,7 +257,17 @@ class CASFederateAuth(AuthUser):
|
|||||||
super(CASFederateAuth, self).__init__(username)
|
super(CASFederateAuth, self).__init__(username)
|
||||||
|
|
||||||
def test_password(self, ticket):
|
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:
|
if not self.user or not self.user.ticket:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -168,7 +278,13 @@ class CASFederateAuth(AuthUser):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def attributs(self):
|
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)
|
if not self.user: # pragma: no cover (should not happen)
|
||||||
return {}
|
return {}
|
||||||
else:
|
else:
|
||||||
|
@ -36,7 +36,7 @@ class CASError(ValueError):
|
|||||||
|
|
||||||
class ReturnUnicode(object):
|
class ReturnUnicode(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unicode(string, charset):
|
def u(string, charset):
|
||||||
if not isinstance(string, six.text_type):
|
if not isinstance(string, six.text_type):
|
||||||
return string.decode(charset)
|
return string.decode(charset)
|
||||||
else:
|
else:
|
||||||
@ -157,7 +157,7 @@ class CASClientV1(CASClientBase, ReturnUnicode):
|
|||||||
charset = content_type.split("charset=")[-1]
|
charset = content_type.split("charset=")[-1]
|
||||||
else:
|
else:
|
||||||
charset = "ascii"
|
charset = "ascii"
|
||||||
user = self.unicode(page.readline().strip(), charset)
|
user = self.u(page.readline().strip(), charset)
|
||||||
return user, None, None
|
return user, None, None
|
||||||
else:
|
else:
|
||||||
return None, None, None
|
return None, None, None
|
||||||
@ -202,18 +202,18 @@ class CASClientV2(CASClientBase, ReturnUnicode):
|
|||||||
def parse_attributes_xml_element(cls, element, charset):
|
def parse_attributes_xml_element(cls, element, charset):
|
||||||
attributes = dict()
|
attributes = dict()
|
||||||
for attribute in element:
|
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 tag in attributes:
|
||||||
if isinstance(attributes[tag], list):
|
if isinstance(attributes[tag], list):
|
||||||
attributes[tag].append(cls.unicode(attribute.text, charset))
|
attributes[tag].append(cls.u(attribute.text, charset))
|
||||||
else:
|
else:
|
||||||
attributes[tag] = [attributes[tag]]
|
attributes[tag] = [attributes[tag]]
|
||||||
attributes[tag].append(cls.unicode(attribute.text, charset))
|
attributes[tag].append(cls.u(attribute.text, charset))
|
||||||
else:
|
else:
|
||||||
if tag == u'attraStyle':
|
if tag == u'attraStyle':
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
attributes[tag] = cls.unicode(attribute.text, charset)
|
attributes[tag] = cls.u(attribute.text, charset)
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -238,9 +238,9 @@ class CASClientV2(CASClientBase, ReturnUnicode):
|
|||||||
if tree[0].tag.endswith('authenticationSuccess'):
|
if tree[0].tag.endswith('authenticationSuccess'):
|
||||||
for element in tree[0]:
|
for element in tree[0]:
|
||||||
if element.tag.endswith('user'):
|
if element.tag.endswith('user'):
|
||||||
user = cls.unicode(element.text, charset)
|
user = cls.u(element.text, charset)
|
||||||
elif element.tag.endswith('proxyGrantingTicket'):
|
elif element.tag.endswith('proxyGrantingTicket'):
|
||||||
pgtiou = cls.unicode(element.text, charset)
|
pgtiou = cls.u(element.text, charset)
|
||||||
elif element.tag.endswith('attributes'):
|
elif element.tag.endswith('attributes'):
|
||||||
attributes = cls.parse_attributes_xml_element(element, charset)
|
attributes = cls.parse_attributes_xml_element(element, charset)
|
||||||
return user, attributes, pgtiou
|
return user, attributes, pgtiou
|
||||||
@ -255,15 +255,15 @@ class CASClientV3(CASClientV2, SingleLogoutMixin):
|
|||||||
def parse_attributes_xml_element(cls, element, charset):
|
def parse_attributes_xml_element(cls, element, charset):
|
||||||
attributes = dict()
|
attributes = dict()
|
||||||
for attribute in element:
|
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 tag in attributes:
|
||||||
if isinstance(attributes[tag], list):
|
if isinstance(attributes[tag], list):
|
||||||
attributes[tag].append(cls.unicode(attribute.text, charset))
|
attributes[tag].append(cls.u(attribute.text, charset))
|
||||||
else:
|
else:
|
||||||
attributes[tag] = [attributes[tag]]
|
attributes[tag] = [attributes[tag]]
|
||||||
attributes[tag].append(cls.unicode(attribute.text, charset))
|
attributes[tag].append(cls.u(attribute.text, charset))
|
||||||
else:
|
else:
|
||||||
attributes[tag] = cls.unicode(attribute.text, charset)
|
attributes[tag] = cls.u(attribute.text, charset)
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -323,25 +323,25 @@ class CASClientWithSAMLV1(CASClientV2, SingleLogoutMixin):
|
|||||||
# User is validated
|
# User is validated
|
||||||
name_identifier = tree.find('.//' + SAML_1_0_ASSERTION_NS + 'NameIdentifier')
|
name_identifier = tree.find('.//' + SAML_1_0_ASSERTION_NS + 'NameIdentifier')
|
||||||
if name_identifier is not None:
|
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')
|
attrs = tree.findall('.//' + SAML_1_0_ASSERTION_NS + 'Attribute')
|
||||||
for at in attrs:
|
for at in attrs:
|
||||||
if self.username_attribute in list(at.attrib.values()):
|
if self.username_attribute in list(at.attrib.values()):
|
||||||
user = self.unicode(
|
user = self.u(
|
||||||
at.find(SAML_1_0_ASSERTION_NS + 'AttributeValue').text,
|
at.find(SAML_1_0_ASSERTION_NS + 'AttributeValue').text,
|
||||||
charset
|
charset
|
||||||
)
|
)
|
||||||
attributes[u'uid'] = user
|
attributes[u'uid'] = user
|
||||||
|
|
||||||
values = at.findall(SAML_1_0_ASSERTION_NS + 'AttributeValue')
|
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:
|
if len(values) > 1:
|
||||||
values_array = []
|
values_array = []
|
||||||
for v in values:
|
for v in values:
|
||||||
values_array.append(self.unicode(v.text, charset))
|
values_array.append(self.u(v.text, charset))
|
||||||
attributes[key] = values_array
|
attributes[key] = values_array
|
||||||
else:
|
else:
|
||||||
attributes[key] = self.unicode(values[0].text, charset)
|
attributes[key] = self.u(values[0].text, charset)
|
||||||
return user, attributes, None
|
return user, attributes, None
|
||||||
finally:
|
finally:
|
||||||
page.close()
|
page.close()
|
||||||
|
@ -13,84 +13,146 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
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)
|
#: A dotted path to a class or a class implementing cas_server.auth.AuthUser.
|
||||||
setting_default('CAS_ST_LEN', settings.CAS_TICKET_LEN)
|
CAS_AUTH_CLASS = 'cas_server.auth.DjangoAuthUser'
|
||||||
setting_default('CAS_PT_LEN', settings.CAS_TICKET_LEN)
|
#: Path to certificate authorities file. Usually on linux the local CAs are in
|
||||||
setting_default('CAS_PGT_LEN', settings.CAS_TICKET_LEN)
|
#: /etc/ssl/certs/ca-certificates.crt. ``True`` tell requests to use its internal certificat
|
||||||
setting_default('CAS_PGTIOU_LEN', settings.CAS_TICKET_LEN)
|
#: 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
|
#: All CAS implementation MUST support ST and PT up to 32 chars,
|
||||||
# if more requests need to be send, there are queued
|
#: PGT and PGTIOU up to 64 chars and it is RECOMMENDED that all
|
||||||
setting_default('CAS_SLO_MAX_PARALLEL_REQUESTS', 10)
|
#: tickets up to 256 chars are supports so we use 64 for the default
|
||||||
# SLO request timeout.
|
#: len.
|
||||||
setting_default('CAS_SLO_TIMEOUT', 5)
|
CAS_TICKET_LEN = 64
|
||||||
|
|
||||||
setting_default('CAS_SQL_HOST', 'localhost')
|
#: alias of :obj:`settings.CAS_TICKET_LEN`
|
||||||
setting_default('CAS_SQL_USERNAME', '')
|
CAS_LT_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN)
|
||||||
setting_default('CAS_SQL_PASSWORD', '')
|
#: alias of :obj:`settings.CAS_TICKET_LEN`
|
||||||
setting_default('CAS_SQL_DBNAME', '')
|
#: Services MUST be able to accept service tickets of up to 32 characters in length.
|
||||||
setting_default('CAS_SQL_DBCHARSET', 'utf8')
|
CAS_ST_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN)
|
||||||
setting_default('CAS_SQL_USER_QUERY', 'SELECT user AS usersame, pass AS '
|
#: alias of :obj:`settings.CAS_TICKET_LEN`
|
||||||
'password, users.* FROM users WHERE user = %s')
|
#: Back-end services MUST be able to accept proxy tickets of up to 32 characters.
|
||||||
setting_default('CAS_SQL_PASSWORD_CHECK', 'crypt') # crypt or plain
|
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')
|
#: Prefix of login tickets.
|
||||||
setting_default('CAS_TEST_PASSWORD', 'test')
|
CAS_LOGIN_TICKET_PREFIX = u'LT'
|
||||||
setting_default(
|
#: Prefix of service tickets. Service tickets MUST begin with the characters ST so you should not
|
||||||
'CAS_TEST_ATTRIBUTES',
|
#: 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',
|
'nom': 'Nymous',
|
||||||
'prenom': 'Ano',
|
'prenom': 'Ano',
|
||||||
'email': 'anonymous@example.net',
|
'email': 'anonymous@example.net',
|
||||||
'alias': ['demo1', 'demo2']
|
'alias': ['demo1', 'demo2']
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
setting_default('CAS_ENABLE_AJAX_AUTH', False)
|
|
||||||
|
|
||||||
setting_default('CAS_FEDERATE', False)
|
#: A :class:`bool` for activatinc the hability to fetch tickets using javascript.
|
||||||
setting_default('CAS_FEDERATE_REMEMBER_TIMEOUT', 604800) # one week
|
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:
|
if settings.CAS_FEDERATE:
|
||||||
settings.CAS_AUTH_CLASS = "cas_server.auth.CASFederateAuth"
|
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
|
# (c) 2016 Valentin Samir
|
||||||
"""federated mode helper classes"""
|
"""federated mode helper classes"""
|
||||||
from .default_settings import settings
|
from .default_settings import SessionStore
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
|
||||||
from .cas import CASClient
|
from .cas import CASClient
|
||||||
from .models import FederatedUser, FederateSLO, User
|
from .models import FederatedUser, FederateSLO, User
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from importlib import import_module
|
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
|
|
||||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
#: logger facility
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CASFederateValidateUser(object):
|
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
|
username = None
|
||||||
|
#: the provider returned attributes
|
||||||
attributs = {}
|
attributs = {}
|
||||||
|
#: the CAS client instance
|
||||||
client = None
|
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):
|
def __init__(self, provider, service_url):
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
@ -41,15 +52,31 @@ class CASFederateValidateUser(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_login_url(self):
|
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()
|
return self.client.get_login_url()
|
||||||
|
|
||||||
def get_logout_url(self, redirect_url=None):
|
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)
|
return self.client.get_logout_url(redirect_url)
|
||||||
|
|
||||||
def verify_ticket(self, ticket):
|
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:
|
try:
|
||||||
username, attributs = self.client.verify_ticket(ticket)[:2]
|
username, attributs = self.client.verify_ticket(ticket)[:2]
|
||||||
except urllib.error.URLError:
|
except urllib.error.URLError:
|
||||||
@ -57,7 +84,7 @@ class CASFederateValidateUser(object):
|
|||||||
if username is not None:
|
if username is not None:
|
||||||
if attributs is None:
|
if attributs is None:
|
||||||
attributs = {}
|
attributs = {}
|
||||||
attributs["provider"] = self.provider
|
attributs["provider"] = self.provider.suffix
|
||||||
self.username = username
|
self.username = username
|
||||||
self.attributs = attributs
|
self.attributs = attributs
|
||||||
user = FederatedUser.objects.update_or_create(
|
user = FederatedUser.objects.update_or_create(
|
||||||
@ -73,7 +100,15 @@ class CASFederateValidateUser(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register_slo(username, session_key, ticket):
|
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:
|
try:
|
||||||
FederateSLO.objects.create(
|
FederateSLO.objects.create(
|
||||||
username=username,
|
username=username,
|
||||||
@ -84,7 +119,14 @@ class CASFederateValidateUser(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def clean_sessions(self, logout_request):
|
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:
|
try:
|
||||||
slos = self.client.get_saml_slos(logout_request) or []
|
slos = self.client.get_saml_slos(logout_request) or []
|
||||||
except NameError: # pragma: no cover (should not happen)
|
except NameError: # pragma: no cover (should not happen)
|
||||||
|
@ -18,21 +18,55 @@ import cas_server.utils as utils
|
|||||||
import cas_server.models as models
|
import cas_server.models as models
|
||||||
|
|
||||||
|
|
||||||
class WarnForm(forms.Form):
|
class BootsrapForm(forms.Form):
|
||||||
"""Form used on warn page before emiting a ticket"""
|
"""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)
|
service = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
|
#: Is the service asking the authentication renewal ?
|
||||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
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)
|
gateway = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
method = 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)
|
warned = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
|
#: A valid LoginTicket to prevent POST replay
|
||||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
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
|
Bases: :class:`django.forms.Form`
|
||||||
allowing the user to choose a identity provider.
|
|
||||||
|
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(
|
provider = forms.ModelChoiceField(
|
||||||
queryset=models.FederatedIendityProvider.objects.filter(display=True).order_by(
|
queryset=models.FederatedIendityProvider.objects.filter(display=True).order_by(
|
||||||
"pos",
|
"pos",
|
||||||
@ -42,27 +76,49 @@ class FederateSelect(forms.Form):
|
|||||||
to_field_name="suffix",
|
to_field_name="suffix",
|
||||||
label=_('Identity provider'),
|
label=_('Identity provider'),
|
||||||
)
|
)
|
||||||
|
#: The service url for which the user want a ticket
|
||||||
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
||||||
method = forms.CharField(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)
|
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)
|
warn = forms.BooleanField(label=_('warn'), required=False)
|
||||||
|
#: Is the service asking the authentication renewal ?
|
||||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
|
|
||||||
class UserCredential(forms.Form):
|
class UserCredential(BootsrapForm):
|
||||||
"""Form used on the login page to retrive user credentials"""
|
"""
|
||||||
|
Bases: :class:`django.forms.Form`
|
||||||
|
|
||||||
|
Form used on the login page to retrive user credentials
|
||||||
|
"""
|
||||||
|
#: The user username
|
||||||
username = forms.CharField(label=_('login'))
|
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)
|
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
||||||
|
#: The user password
|
||||||
password = forms.CharField(label=_('password'), widget=forms.PasswordInput)
|
password = forms.CharField(label=_('password'), widget=forms.PasswordInput)
|
||||||
|
#: A valid LoginTicket to prevent POST replay
|
||||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
method = 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)
|
warn = forms.BooleanField(label=_('warn'), required=False)
|
||||||
|
#: Is the service asking the authentication renewal ?
|
||||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(UserCredential, self).__init__(*args, **kwargs)
|
super(UserCredential, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def clean(self):
|
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()
|
cleaned_data = super(UserCredential, self).clean()
|
||||||
auth = utils.import_attr(settings.CAS_AUTH_CLASS)(cleaned_data.get("username"))
|
auth = utils.import_attr(settings.CAS_AUTH_CLASS)(cleaned_data.get("username"))
|
||||||
if auth.test_password(cleaned_data.get("password")):
|
if auth.test_password(cleaned_data.get("password")):
|
||||||
@ -73,17 +129,51 @@ class UserCredential(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class FederateUserCredential(UserCredential):
|
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())
|
username = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
#: The service url for which the user want a ticket
|
||||||
service = forms.CharField(widget=forms.HiddenInput(), required=False)
|
service = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
|
#: The ``ticket`` used to authenticate the user against a provider
|
||||||
password = forms.CharField(widget=forms.HiddenInput())
|
password = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
#: alias of :attr:`password`
|
||||||
ticket = forms.CharField(widget=forms.HiddenInput())
|
ticket = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
#: A valid LoginTicket to prevent POST replay
|
||||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
method = 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)
|
warn = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
|
#: Is the service asking the authentication renewal ?
|
||||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
def clean(self):
|
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()
|
cleaned_data = super(FederateUserCredential, self).clean()
|
||||||
try:
|
try:
|
||||||
user = models.FederatedUser.get_from_federated_username(cleaned_data["username"])
|
user = models.FederatedUser.get_from_federated_username(cleaned_data["username"])
|
||||||
@ -99,7 +189,11 @@ class FederateUserCredential(UserCredential):
|
|||||||
|
|
||||||
|
|
||||||
class TicketForm(forms.ModelForm):
|
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:
|
class Meta:
|
||||||
model = models.Ticket
|
model = models.Ticket
|
||||||
exclude = []
|
exclude = []
|
||||||
|
@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
|||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import cas_server.utils
|
import cas_server.utils
|
||||||
import picklefield.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -31,7 +30,7 @@ class Migration(migrations.Migration):
|
|||||||
name='ProxyGrantingTicket',
|
name='ProxyGrantingTicket',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
('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)),
|
('validate', models.BooleanField(default=False)),
|
||||||
('service', models.TextField()),
|
('service', models.TextField()),
|
||||||
('creation', models.DateTimeField(auto_now_add=True)),
|
('creation', models.DateTimeField(auto_now_add=True)),
|
||||||
@ -47,7 +46,7 @@ class Migration(migrations.Migration):
|
|||||||
name='ProxyTicket',
|
name='ProxyTicket',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
('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)),
|
('validate', models.BooleanField(default=False)),
|
||||||
('service', models.TextField()),
|
('service', models.TextField()),
|
||||||
('creation', models.DateTimeField(auto_now_add=True)),
|
('creation', models.DateTimeField(auto_now_add=True)),
|
||||||
@ -80,7 +79,7 @@ class Migration(migrations.Migration):
|
|||||||
name='ServiceTicket',
|
name='ServiceTicket',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
('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)),
|
('validate', models.BooleanField(default=False)),
|
||||||
('service', models.TextField()),
|
('service', models.TextField()),
|
||||||
('creation', models.DateTimeField(auto_now_add=True)),
|
('creation', models.DateTimeField(auto_now_add=True)),
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import picklefield.fields
|
|
||||||
import django.db.models.deletion
|
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')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('username', models.CharField(max_length=124)),
|
('username', models.CharField(max_length=124)),
|
||||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cas_server.FederatedIendityProvider')),
|
('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)),
|
('ticket', models.CharField(max_length=255)),
|
||||||
('last_update', models.DateTimeField(auto_now=True)),
|
('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
|
# (c) 2015-2016 Valentin Samir
|
||||||
"""models for the app"""
|
"""models for the app"""
|
||||||
from .default_settings import settings
|
from .default_settings import settings, SessionStore
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
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.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from picklefield.fields import PickledObjectField
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from importlib import import_module
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from requests_futures.sessions import FuturesSession
|
from requests_futures.sessions import FuturesSession
|
||||||
|
|
||||||
import cas_server.utils as utils
|
import cas_server.utils as utils
|
||||||
|
|
||||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
#: logger facility
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class FederatedIendityProvider(models.Model):
|
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:
|
class Meta:
|
||||||
verbose_name = _("identity provider")
|
verbose_name = _(u"identity provider")
|
||||||
verbose_name_plural = _("identity providers")
|
verbose_name_plural = _(u"identity providers")
|
||||||
|
#: Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``.
|
||||||
|
#: it must be unique.
|
||||||
suffix = models.CharField(
|
suffix = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name=_(u"suffix"),
|
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"))
|
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(
|
cas_protocol_version = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
choices=[
|
choices=[
|
||||||
@ -57,28 +67,37 @@ class FederatedIendityProvider(models.Model):
|
|||||||
("CAS_2_SAML_1_0", "SAML 1.1")
|
("CAS_2_SAML_1_0", "SAML 1.1")
|
||||||
],
|
],
|
||||||
verbose_name=_(u"CAS protocol version"),
|
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"
|
default="3"
|
||||||
)
|
)
|
||||||
|
#: Name for this identity provider displayed on the login page.
|
||||||
verbose_name = models.CharField(
|
verbose_name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_(u"verbose name"),
|
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(
|
pos = models.IntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
verbose_name=_(u"position"),
|
verbose_name=_(u"position"),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
(
|
(
|
||||||
|
u"Position of the identity provider on the login page. "
|
||||||
u"Identity provider are sorted using the "
|
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(
|
display = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name=_(u"display"),
|
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):
|
def __str__(self):
|
||||||
@ -86,36 +105,72 @@ class FederatedIendityProvider(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_username_from_suffix(username, suffix):
|
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)
|
return u'%s@%s' % (username, suffix)
|
||||||
|
|
||||||
def build_username(self, username):
|
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)
|
return u'%s@%s' % (username, self.suffix)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class FederatedUser(models.Model):
|
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:
|
class Meta:
|
||||||
unique_together = ("username", "provider")
|
unique_together = ("username", "provider")
|
||||||
|
#: The user username returned by the CAS backend on successful ticket validation
|
||||||
username = models.CharField(max_length=124)
|
username = models.CharField(max_length=124)
|
||||||
|
#: A foreign key to :class:`FederatedIendityProvider`
|
||||||
provider = models.ForeignKey(FederatedIendityProvider, on_delete=models.CASCADE)
|
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)
|
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)
|
last_update = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.federated_username
|
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
|
@property
|
||||||
def federated_username(self):
|
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)
|
return self.provider.build_username(self.username)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_from_federated_username(cls, username):
|
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:
|
if username is None:
|
||||||
raise cls.DoesNotExist()
|
raise cls.DoesNotExist()
|
||||||
else:
|
else:
|
||||||
@ -130,7 +185,7 @@ class FederatedUser(models.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean_old_entries(cls):
|
def clean_old_entries(cls):
|
||||||
"""remove old unused federated users"""
|
"""remove old unused :class:`FederatedUser`"""
|
||||||
federated_users = cls.objects.filter(
|
federated_users = cls.objects.filter(
|
||||||
last_update__lt=(timezone.now() - timedelta(seconds=settings.CAS_TICKET_TIMEOUT))
|
last_update__lt=(timezone.now() - timedelta(seconds=settings.CAS_TICKET_TIMEOUT))
|
||||||
)
|
)
|
||||||
@ -141,16 +196,23 @@ class FederatedUser(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class FederateSLO(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:
|
class Meta:
|
||||||
unique_together = ("username", "session_key", "ticket")
|
unique_together = ("username", "session_key", "ticket")
|
||||||
|
#: the federated username with the ``@``component
|
||||||
username = models.CharField(max_length=30)
|
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)
|
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)
|
ticket = models.CharField(max_length=255, db_index=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean_deleted_sessions(cls):
|
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():
|
for federate_slo in cls.objects.all():
|
||||||
if not SessionStore(session_key=federate_slo.session_key).get('authenticated'):
|
if not SessionStore(session_key=federate_slo.session_key).get('authenticated'):
|
||||||
federate_slo.delete()
|
federate_slo.delete()
|
||||||
@ -158,17 +220,27 @@ class FederateSLO(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class User(models.Model):
|
class User(models.Model):
|
||||||
"""A user logged into the CAS"""
|
"""
|
||||||
|
Bases: :class:`django.db.models.Model`
|
||||||
|
|
||||||
|
A user logged into the CAS
|
||||||
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("username", "session_key")
|
unique_together = ("username", "session_key")
|
||||||
verbose_name = _("User")
|
verbose_name = _("User")
|
||||||
verbose_name_plural = _("Users")
|
verbose_name_plural = _("Users")
|
||||||
|
#: The session key of the current authenticated user
|
||||||
session_key = models.CharField(max_length=40, blank=True, null=True)
|
session_key = models.CharField(max_length=40, blank=True, null=True)
|
||||||
|
#: The username of the current authenticated user
|
||||||
username = models.CharField(max_length=30)
|
username = models.CharField(max_length=30)
|
||||||
|
#: Last time the authenticated user has do something (auth, fetch ticket, etc…)
|
||||||
date = models.DateTimeField(auto_now=True)
|
date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
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:
|
if settings.CAS_FEDERATE:
|
||||||
FederateSLO.objects.filter(
|
FederateSLO.objects.filter(
|
||||||
username=self.username,
|
username=self.username,
|
||||||
@ -178,7 +250,10 @@ class User(models.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean_old_entries(cls):
|
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(
|
users = cls.objects.filter(
|
||||||
date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE))
|
date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE))
|
||||||
)
|
)
|
||||||
@ -188,7 +263,7 @@ class User(models.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean_deleted_sessions(cls):
|
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():
|
for user in cls.objects.all():
|
||||||
if not SessionStore(session_key=user.session_key).get('authenticated'):
|
if not SessionStore(session_key=user.session_key).get('authenticated'):
|
||||||
user.logout()
|
user.logout()
|
||||||
@ -196,14 +271,22 @@ class User(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def attributs(self):
|
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()
|
return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u"%s - %s" % (self.username, self.session_key)
|
return u"%s - %s" % (self.username, self.session_key)
|
||||||
|
|
||||||
def logout(self, request=None):
|
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 = []
|
async_list = []
|
||||||
session = FuturesSession(
|
session = FuturesSession(
|
||||||
executor=ThreadPoolExecutor(max_workers=settings.CAS_SLO_MAX_PARALLEL_REQUESTS)
|
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):
|
def get_ticket(self, ticket_class, service, service_pattern, renew):
|
||||||
"""
|
"""
|
||||||
Generate a ticket using `ticket_class` for the service
|
Generate a ticket using ``ticket_class`` for the service
|
||||||
`service` matching `service_pattern` and asking or not for
|
``service`` matching ``service_pattern`` and asking or not for
|
||||||
authentication renewal with `renew
|
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(
|
attributs = dict(
|
||||||
(a.name, a.replace if a.replace else a.name) for a in service_pattern.attributs.all()
|
(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
|
return ticket
|
||||||
|
|
||||||
def get_service_url(self, service, service_pattern, renew):
|
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)
|
ticket = self.get_ticket(ServiceTicket, service, service_pattern, renew)
|
||||||
url = utils.update_url(service, {'ticket': ticket.value})
|
url = utils.update_url(service, {'ticket': ticket.value})
|
||||||
logger.info("Service ticket created for service %s by user %s." % (service, self.username))
|
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):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BadUsername(ServicePatternException):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BadFilter(ServicePatternException):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserFieldNotDefined(ServicePatternException):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ServicePattern(models.Model):
|
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:
|
class Meta:
|
||||||
ordering = ("pos", )
|
ordering = ("pos", )
|
||||||
verbose_name = _("Service pattern")
|
verbose_name = _("Service pattern")
|
||||||
verbose_name_plural = _("Services patterns")
|
verbose_name_plural = _("Services patterns")
|
||||||
|
|
||||||
|
#: service patterns are sorted using the :attr:`pos` attribute
|
||||||
pos = models.IntegerField(
|
pos = models.IntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
verbose_name=_(u"position"),
|
verbose_name=_(u"position"),
|
||||||
help_text=_(u"service patterns are sorted using the position attribute")
|
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(
|
name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
@ -325,6 +452,9 @@ class ServicePattern(models.Model):
|
|||||||
verbose_name=_(u"name"),
|
verbose_name=_(u"name"),
|
||||||
help_text=_(u"A name for the service")
|
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(
|
pattern = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
@ -335,6 +465,7 @@ class ServicePattern(models.Model):
|
|||||||
"As it is a regular expression, special character must be escaped with a '\\'."
|
"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(
|
user_field = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
default="",
|
default="",
|
||||||
@ -342,27 +473,35 @@ class ServicePattern(models.Model):
|
|||||||
verbose_name=_(u"user field"),
|
verbose_name=_(u"user field"),
|
||||||
help_text=_("Name of the attribut to transmit as username, empty = login")
|
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(
|
restrict_users = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_(u"restrict username"),
|
verbose_name=_(u"restrict username"),
|
||||||
help_text=_("Limit username allowed to connect to the list provided bellow")
|
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(
|
proxy = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_(u"proxy"),
|
verbose_name=_(u"proxy"),
|
||||||
help_text=_("Proxy tickets can be delivered to the service")
|
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(
|
proxy_callback = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_(u"proxy callback"),
|
verbose_name=_(u"proxy callback"),
|
||||||
help_text=_("can be used as a proxy callback to deliver PGT")
|
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(
|
single_log_out = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_(u"single log out"),
|
verbose_name=_(u"single log out"),
|
||||||
help_text=_("Enable SLO for the service")
|
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(
|
single_log_out_callback = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
default="",
|
default="",
|
||||||
@ -376,7 +515,20 @@ class ServicePattern(models.Model):
|
|||||||
return u"%s: %s" % (self.pos, self.pattern)
|
return u"%s: %s" % (self.pos, self.pattern)
|
||||||
|
|
||||||
def check_user(self, user):
|
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):
|
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))
|
logger.warning("Username %s not allowed on service %s" % (user.username, self.name))
|
||||||
raise BadUsername()
|
raise BadUsername()
|
||||||
@ -416,8 +568,15 @@ class ServicePattern(models.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, service):
|
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'):
|
for service_pattern in cls.objects.all().order_by('pos'):
|
||||||
if re.match(service_pattern.pattern, service):
|
if re.match(service_pattern.pattern, service):
|
||||||
return service_pattern
|
return service_pattern
|
||||||
@ -427,12 +586,20 @@ class ServicePattern(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Username(models.Model):
|
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(
|
value = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_(u"username"),
|
verbose_name=_(u"username"),
|
||||||
help_text=_(u"username allowed to connect to the service")
|
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")
|
service_pattern = models.ForeignKey(ServicePattern, related_name="usernames")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -441,14 +608,23 @@ class Username(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ReplaceAttributName(models.Model):
|
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:
|
class Meta:
|
||||||
unique_together = ('name', 'replace', 'service_pattern')
|
unique_together = ('name', 'replace', 'service_pattern')
|
||||||
|
#: Name the attribute: a key of :attr:`User.attributs`
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_(u"name"),
|
verbose_name=_(u"name"),
|
||||||
help_text=_(u"name of an attribut to send to the service, use * for all attributes")
|
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(
|
replace = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -456,6 +632,9 @@ class ReplaceAttributName(models.Model):
|
|||||||
help_text=_(u"name under which the attribut will be show"
|
help_text=_(u"name under which the attribut will be show"
|
||||||
u"to the service. empty = default name of the attribut")
|
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")
|
service_pattern = models.ForeignKey(ServicePattern, related_name="attributs")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -467,17 +646,29 @@ class ReplaceAttributName(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class FilterAttributValue(models.Model):
|
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(
|
attribut = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_(u"attribut"),
|
verbose_name=_(u"attribut"),
|
||||||
help_text=_(u"Name of the attribut which must verify pattern")
|
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(
|
pattern = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_(u"pattern"),
|
verbose_name=_(u"pattern"),
|
||||||
help_text=_(u"a regular expression")
|
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")
|
service_pattern = models.ForeignKey(ServicePattern, related_name="filters")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -486,23 +677,34 @@ class FilterAttributValue(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ReplaceAttributValue(models.Model):
|
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(
|
attribut = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_(u"attribut"),
|
verbose_name=_(u"attribut"),
|
||||||
help_text=_(u"Name of the attribut for which the value must be replace")
|
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(
|
pattern = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_(u"pattern"),
|
verbose_name=_(u"pattern"),
|
||||||
help_text=_(u"An regular expression maching whats need to be replaced")
|
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(
|
replace = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_(u"replace"),
|
verbose_name=_(u"replace"),
|
||||||
help_text=_(u"replace expression, groups are capture by \\1, \\2 …")
|
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")
|
service_pattern = models.ForeignKey(ServicePattern, related_name="replacements")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -511,21 +713,54 @@ class ReplaceAttributValue(models.Model):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Ticket(models.Model):
|
class Ticket(models.Model):
|
||||||
"""Generic class for a Ticket"""
|
"""
|
||||||
|
Bases: :class:`django.db.models.Model`
|
||||||
|
|
||||||
|
Generic class for a Ticket
|
||||||
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
#: ForeignKey to a :class:`User`.
|
||||||
user = models.ForeignKey(User, related_name="%(class)s")
|
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)
|
validate = models.BooleanField(default=False)
|
||||||
|
#: The service url for the ticket
|
||||||
service = models.TextField()
|
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")
|
service_pattern = models.ForeignKey(ServicePattern, related_name="%(class)s")
|
||||||
|
#: Date of the ticket creation
|
||||||
creation = models.DateTimeField(auto_now_add=True)
|
creation = models.DateTimeField(auto_now_add=True)
|
||||||
|
#: A boolean. ``True`` if the user has just renew his authentication
|
||||||
renew = models.BooleanField(default=False)
|
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)
|
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
|
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
|
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):
|
def __str__(self):
|
||||||
return u"Ticket-%s" % self.pk
|
return u"Ticket-%s" % self.pk
|
||||||
|
|
||||||
@ -596,16 +831,119 @@ class Ticket(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_class(ticket):
|
def get_class(ticket, classes=None):
|
||||||
for ticket_class in [ServiceTicket, ProxyTicket, ProxyGrantingTicket]:
|
"""
|
||||||
|
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):
|
if ticket.startswith(ticket_class.PREFIX):
|
||||||
return ticket_class
|
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
|
@python_2_unicode_compatible
|
||||||
class ServiceTicket(Ticket):
|
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
|
PREFIX = settings.CAS_SERVICE_TICKET_PREFIX
|
||||||
|
#: The ticket value
|
||||||
value = models.CharField(max_length=255, default=utils.gen_st, unique=True)
|
value = models.CharField(max_length=255, default=utils.gen_st, unique=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -614,8 +952,14 @@ class ServiceTicket(Ticket):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ProxyTicket(Ticket):
|
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
|
PREFIX = settings.CAS_PROXY_TICKET_PREFIX
|
||||||
|
#: The ticket value
|
||||||
value = models.CharField(max_length=255, default=utils.gen_pt, unique=True)
|
value = models.CharField(max_length=255, default=utils.gen_pt, unique=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -624,9 +968,17 @@ class ProxyTicket(Ticket):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ProxyGrantingTicket(Ticket):
|
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
|
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
|
VALIDITY = settings.CAS_PGT_VALIDITY
|
||||||
|
#: The ticket value
|
||||||
value = models.CharField(max_length=255, default=utils.gen_pgt, unique=True)
|
value = models.CharField(max_length=255, default=utils.gen_pgt, unique=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -635,10 +987,18 @@ class ProxyGrantingTicket(Ticket):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Proxy(models.Model):
|
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:
|
class Meta:
|
||||||
ordering = ("-pk", )
|
ordering = ("-pk", )
|
||||||
|
#: Service url of the PGT used for getting the associated :class:`ProxyTicket`
|
||||||
url = models.CharField(max_length=255)
|
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")
|
proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -1,36 +1,63 @@
|
|||||||
{% extends 'bootstrap3/bootstrap3.html' %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block bootstrap3_title %}{% block title %}{% trans "Central Authentication Service" %}{% endblock %}{% endblock %}
|
|
||||||
|
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
{% load bootstrap3 %}
|
<!DOCTYPE html>
|
||||||
|
<html{% if request.LANGUAGE_CODE %} lang="{{ request.LANGUAGE_CODE }}"{% endif %}>
|
||||||
{% block bootstrap3_extra_head %}
|
<head>
|
||||||
<link rel="shortcut icon" href="/static/cas_server/favicon.ico?v=1" />
|
<meta charset="utf-8">
|
||||||
<link href="{% static "cas_server/login.css" %}" rel="stylesheet">
|
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge" /><![endif]-->
|
||||||
{% endblock %}
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}{% trans "Central Authentication Service" %}{% endblock %}</title>
|
||||||
{% block bootstrap3_content %}
|
<link href="{{settings.CAS_COMPONENT_URLS.bootstrap3_css}}" rel="stylesheet">
|
||||||
<div class="container">
|
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||||
{% if auto_submit %}<noscript>{% endif %}
|
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||||
<div class="row">
|
<!--[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">
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<h1 id="app-name">
|
<h1 id="app-name">
|
||||||
{% if settings.CAS_LOGO_URL %}<img src="{{settings.CAS_LOGO_URL}}"></img> {% endif %}
|
{% if settings.CAS_LOGO_URL %}<img src="{{settings.CAS_LOGO_URL}}"></img> {% endif %}
|
||||||
{% trans "Central Authentication Service" %}</h1>
|
{% trans "Central Authentication Service" %}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if auto_submit %}</noscript>{% endif %}
|
{% if auto_submit %}</noscript>{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
|
<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">
|
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
|
||||||
{% if auto_submit %}<noscript>{% endif %}
|
{% block ante_messages %}{% endblock %}
|
||||||
{% bootstrap_messages %}
|
{% if auto_submit %}<noscript>{% endif %}
|
||||||
{% if auto_submit %}</noscript>{% endif %}
|
{% for message in messages %}
|
||||||
{% block content %}
|
<div {% spaceless %}
|
||||||
{% endblock %}
|
{% if message.level == message_levels.DEBUG %}
|
||||||
</div>
|
class="alert alert-warning alert-dismissable"
|
||||||
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-0"></div>
|
{% elif message.level == message_levels.INFO %}
|
||||||
</div>
|
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 -->
|
</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" %}
|
{% extends "cas_server/base.html" %}
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% load staticfiles %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="alert alert-success" role="alert">{% trans "Logged" %}</div>
|
<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" %}
|
<input type="checkbox" name="all" value="1"> {% trans "Log me out from all my sessions" %}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
{% extends "cas_server/base.html" %}
|
{% extends "cas_server/base.html" %}
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% load staticfiles %}
|
|
||||||
{% load i18n %}
|
{% 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 %}
|
{% block content %}
|
||||||
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
|
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
|
||||||
{% if auto_submit %}<noscript>{% endif %}
|
{% csrf_token %}
|
||||||
<h2 class="form-signin-heading">{% trans "Please log in" %}</h2>
|
{% include "cas_server/form.html" %}
|
||||||
{% if auto_submit %}</noscript>{% endif %}
|
{% if auto_submit %}<noscript>{% endif %}
|
||||||
{% csrf_token %}
|
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button>
|
||||||
{% bootstrap_form form %}
|
{% if auto_submit %}</noscript>{% endif %}
|
||||||
{% if auto_submit %}<noscript>{% endif %}
|
</form>
|
||||||
{% bootstrap_button _('Login') size='lg' button_type="submit" button_class="btn-primary btn-block"%}
|
|
||||||
{% if auto_submit %}</noscript>{% endif %}
|
|
||||||
</form>
|
|
||||||
{% if auto_submit %}
|
{% if auto_submit %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
document.getElementById('login_form').submit(); // SUBMIT FORM
|
document.getElementById('login_form').submit(); // SUBMIT FORM
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{% extends "cas_server/base.html" %}
|
{% extends "cas_server/base.html" %}
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
{% extends "cas_server/base.html" %}
|
{% extends "cas_server/base.html" %}
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="form-signin" method="post">
|
<form class="form-signin" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form form %}
|
{% include "cas_server/form.html" %}
|
||||||
{% bootstrap_button _('Connect to the service') size='lg' button_type="submit" button_class="btn-primary btn-block"%}
|
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Connect to the service" %}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -37,7 +37,6 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'bootstrap3',
|
|
||||||
'cas_server',
|
'cas_server',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -993,7 +993,7 @@ class ValidateTestCase(TestCase):
|
|||||||
def test_validate_service_renew(self):
|
def test_validate_service_renew(self):
|
||||||
"""test with a valid (ticket, service) asking for auth renewal"""
|
"""test with a valid (ticket, service) asking for auth renewal"""
|
||||||
# case 1 client is renewing and service ask for renew
|
# 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)
|
self.assertEqual(response.status_code, 302)
|
||||||
ticket_value = response['Location'].split('ticket=')[-1]
|
ticket_value = response['Location'].split('ticket=')[-1]
|
||||||
# get a bare client
|
# get a bare client
|
||||||
@ -1112,7 +1112,9 @@ class ValidateServiceTestCase(TestCase, XmlContent):
|
|||||||
name="localhost",
|
name="localhost",
|
||||||
pattern="^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$",
|
pattern="^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$",
|
||||||
# allow to request PGT by the service
|
# 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)
|
# tell the service pattern to transmit all the user attributes (* is a joker)
|
||||||
models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern)
|
models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern)
|
||||||
@ -1190,10 +1192,30 @@ class ValidateServiceTestCase(TestCase, XmlContent):
|
|||||||
# the attributes settings.CAS_TEST_ATTRIBUTES
|
# the attributes settings.CAS_TEST_ATTRIBUTES
|
||||||
self.assert_success(response, settings.CAS_TEST_USER, 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):
|
def test_validate_service_renew(self):
|
||||||
"""test with a valid (ticket, service) asking for auth renewal"""
|
"""test with a valid (ticket, service) asking for auth renewal"""
|
||||||
# case 1 client is renewing and service ask for renew
|
# 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)
|
self.assertEqual(response.status_code, 302)
|
||||||
ticket_value = response['Location'].split('ticket=')[-1]
|
ticket_value = response['Location'].split('ticket=')[-1]
|
||||||
# get a bare client
|
# get a bare client
|
||||||
|
@ -10,14 +10,13 @@
|
|||||||
#
|
#
|
||||||
# (c) 2015-2016 Valentin Samir
|
# (c) 2015-2016 Valentin Samir
|
||||||
"""urls for the app"""
|
"""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.generic import RedirectView
|
||||||
from django.views.decorators.debug import sensitive_post_parameters, sensitive_variables
|
from django.views.decorators.debug import sensitive_post_parameters, sensitive_variables
|
||||||
|
|
||||||
from cas_server import views
|
from cas_server import views
|
||||||
|
|
||||||
urlpatterns = patterns(
|
urlpatterns = [
|
||||||
'',
|
|
||||||
url(r'^$', RedirectView.as_view(pattern_name="cas_server:login")),
|
url(r'^$', RedirectView.as_view(pattern_name="cas_server:login")),
|
||||||
url(
|
url(
|
||||||
'^login$',
|
'^login$',
|
||||||
@ -60,4 +59,4 @@ urlpatterns = patterns(
|
|||||||
name='auth'
|
name='auth'
|
||||||
),
|
),
|
||||||
url("^federate(?:/(?P<provider>([^/]+)))?$", views.FederateAuth.as_view(), name='federateAuth'),
|
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.core.urlresolvers import reverse
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
from django.contrib import messages
|
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 random
|
||||||
import string
|
import string
|
||||||
@ -29,14 +31,38 @@ from datetime import datetime, timedelta
|
|||||||
from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
|
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):
|
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["settings"] = settings
|
||||||
|
params["message_levels"] = DEFAULT_MESSAGE_LEVELS
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def json_response(request, data):
|
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"] = []
|
data["messages"] = []
|
||||||
for msg in messages.get_messages(request):
|
for msg in messages.get_messages(request):
|
||||||
data["messages"].append({'message': msg.message, 'level': msg.level_tag})
|
data["messages"].append({'message': msg.message, 'level': msg.level_tag})
|
||||||
@ -44,7 +70,13 @@ def json_response(request, data):
|
|||||||
|
|
||||||
|
|
||||||
def import_attr(path):
|
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):
|
if not isinstance(path, str):
|
||||||
return path
|
return path
|
||||||
if "." not in path:
|
if "." not in path:
|
||||||
@ -59,24 +91,50 @@ def import_attr(path):
|
|||||||
|
|
||||||
|
|
||||||
def redirect_params(url_name, params=None):
|
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)
|
url = reverse(url_name)
|
||||||
params = urlencode(params if params else {})
|
params = urlencode(params if params else {})
|
||||||
return HttpResponseRedirect(url + "?%s" % params)
|
return HttpResponseRedirect(url + "?%s" % params)
|
||||||
|
|
||||||
|
|
||||||
def reverse_params(url_name, params=None, **kwargs):
|
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)
|
url = reverse(url_name, **kwargs)
|
||||||
params = urlencode(params if params else {})
|
params = urlencode(params if params else {})
|
||||||
if params:
|
if params:
|
||||||
return url + "?%s" % params
|
return u"%s?%s" % (url, params)
|
||||||
else:
|
else:
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def copy_params(get_or_post_params, ignore=None):
|
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:
|
if ignore is None:
|
||||||
ignore = set()
|
ignore = set()
|
||||||
params = {}
|
params = {}
|
||||||
@ -87,7 +145,14 @@ def copy_params(get_or_post_params, ignore=None):
|
|||||||
|
|
||||||
|
|
||||||
def set_cookie(response, key, value, max_age):
|
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(
|
expires = datetime.strftime(
|
||||||
datetime.utcnow() + timedelta(seconds=max_age),
|
datetime.utcnow() + timedelta(seconds=max_age),
|
||||||
"%a, %d-%b-%Y %H:%M:%S GMT"
|
"%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):
|
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:
|
if ignore_params is None:
|
||||||
ignore_params = set()
|
ignore_params = set()
|
||||||
protocol = 'https' if request.is_secure() else "http"
|
protocol = u'https' if request.is_secure() else u"http"
|
||||||
service_url = "%s://%s%s" % (protocol, request.get_host(), request.path)
|
service_url = u"%s://%s%s" % (protocol, request.get_host(), request.path)
|
||||||
if request.GET:
|
if request.GET:
|
||||||
params = copy_params(request.GET, ignore_params)
|
params = copy_params(request.GET, ignore_params)
|
||||||
if params:
|
if params:
|
||||||
service_url += "?%s" % urlencode(params)
|
service_url += u"?%s" % urlencode(params)
|
||||||
return service_url
|
return service_url
|
||||||
|
|
||||||
|
|
||||||
def update_url(url, params):
|
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):
|
if not isinstance(url, bytes):
|
||||||
url = url.encode('utf-8')
|
url = url.encode('utf-8')
|
||||||
for key, value in list(params.items()):
|
for key, value in list(params.items()):
|
||||||
@ -140,7 +221,12 @@ def update_url(url, params):
|
|||||||
|
|
||||||
|
|
||||||
def unpack_nested_exception(error):
|
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
|
i = 0
|
||||||
while True:
|
while True:
|
||||||
if error.args[i:]:
|
if error.args[i:]:
|
||||||
@ -154,52 +240,97 @@ def unpack_nested_exception(error):
|
|||||||
return error
|
return error
|
||||||
|
|
||||||
|
|
||||||
def _gen_ticket(prefix, lg=settings.CAS_TICKET_LEN):
|
def _gen_ticket(prefix=None, lg=settings.CAS_TICKET_LEN):
|
||||||
"""Generate a ticket with prefix `prefix`"""
|
"""
|
||||||
return '%s-%s' % (
|
Generate a ticket with prefix ``prefix`` and length ``lg``
|
||||||
prefix,
|
|
||||||
''.join(
|
: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(
|
random.choice(
|
||||||
string.ascii_letters + string.digits
|
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():
|
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)
|
return _gen_ticket(settings.CAS_LOGIN_TICKET_PREFIX, settings.CAS_LT_LEN)
|
||||||
|
|
||||||
|
|
||||||
def gen_st():
|
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)
|
return _gen_ticket(settings.CAS_SERVICE_TICKET_PREFIX, settings.CAS_ST_LEN)
|
||||||
|
|
||||||
|
|
||||||
def gen_pt():
|
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)
|
return _gen_ticket(settings.CAS_PROXY_TICKET_PREFIX, settings.CAS_PT_LEN)
|
||||||
|
|
||||||
|
|
||||||
def gen_pgt():
|
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)
|
return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_PREFIX, settings.CAS_PGT_LEN)
|
||||||
|
|
||||||
|
|
||||||
def gen_pgtiou():
|
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)
|
return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX, settings.CAS_PGTIOU_LEN)
|
||||||
|
|
||||||
|
|
||||||
def gen_saml_id():
|
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):
|
def get_tuple(nuplet, index, default=None):
|
||||||
"""
|
"""
|
||||||
return the value in index `index` of the tuple `nuplet` if it exists,
|
:param tuple nuplet: A tuple
|
||||||
else return `default`
|
:param int index: An index
|
||||||
|
:param default: An optional default value
|
||||||
|
:return: ``nuplet[index]`` if defined, else ``default`` (possibly ``None``)
|
||||||
"""
|
"""
|
||||||
if nuplet is None:
|
if nuplet is None:
|
||||||
return default
|
return default
|
||||||
@ -210,7 +341,13 @@ def get_tuple(nuplet, index, default=None):
|
|||||||
|
|
||||||
|
|
||||||
def crypt_salt_is_valid(salt):
|
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:
|
if len(salt) < 2:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -231,11 +368,17 @@ def crypt_salt_is_valid(salt):
|
|||||||
|
|
||||||
|
|
||||||
class LdapHashUserPassword(object):
|
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}"}
|
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}"}
|
schemes_nosalt = {b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"}
|
||||||
|
|
||||||
|
#: map beetween scheme and hash function
|
||||||
_schemes_to_hash = {
|
_schemes_to_hash = {
|
||||||
b"{SMD5}": hashlib.md5,
|
b"{SMD5}": hashlib.md5,
|
||||||
b"{MD5}": hashlib.md5,
|
b"{MD5}": hashlib.md5,
|
||||||
@ -249,6 +392,7 @@ class LdapHashUserPassword(object):
|
|||||||
b"{SHA512}": hashlib.sha512
|
b"{SHA512}": hashlib.sha512
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#: map between scheme and hash length
|
||||||
_schemes_to_len = {
|
_schemes_to_len = {
|
||||||
b"{SMD5}": 16,
|
b"{SMD5}": 16,
|
||||||
b"{SSHA}": 20,
|
b"{SSHA}": 20,
|
||||||
@ -258,7 +402,10 @@ class LdapHashUserPassword(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class BadScheme(ValueError):
|
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
|
pass
|
||||||
|
|
||||||
class BadHash(ValueError):
|
class BadHash(ValueError):
|
||||||
@ -266,14 +413,19 @@ class LdapHashUserPassword(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class BadSalt(ValueError):
|
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
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _raise_bad_scheme(cls, scheme, valid, msg):
|
def _raise_bad_scheme(cls, scheme, valid, msg):
|
||||||
"""
|
"""
|
||||||
Raise BadScheme error for `scheme`, possible valid scheme are
|
Raise :attr:`BadScheme` error for ``scheme``, possible valid scheme are
|
||||||
in `valid`, the error message is `msg`
|
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 = [s.decode() for s in valid]
|
||||||
valid_schemes.sort()
|
valid_schemes.sort()
|
||||||
@ -281,7 +433,12 @@ class LdapHashUserPassword(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _test_scheme(cls, scheme):
|
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:
|
if scheme not in cls.schemes_salt and scheme not in cls.schemes_nosalt:
|
||||||
cls._raise_bad_scheme(
|
cls._raise_bad_scheme(
|
||||||
scheme,
|
scheme,
|
||||||
@ -291,7 +448,12 @@ class LdapHashUserPassword(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _test_scheme_salt(cls, scheme):
|
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:
|
if scheme not in cls.schemes_salt:
|
||||||
cls._raise_bad_scheme(
|
cls._raise_bad_scheme(
|
||||||
scheme,
|
scheme,
|
||||||
@ -301,7 +463,12 @@ class LdapHashUserPassword(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _test_scheme_nosalt(cls, scheme):
|
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:
|
if scheme not in cls.schemes_nosalt:
|
||||||
cls._raise_bad_scheme(
|
cls._raise_bad_scheme(
|
||||||
scheme,
|
scheme,
|
||||||
@ -312,8 +479,15 @@ class LdapHashUserPassword(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def hash(cls, scheme, password, salt=None, charset="utf8"):
|
def hash(cls, scheme, password, salt=None, charset="utf8"):
|
||||||
"""
|
"""
|
||||||
Hash `password` with `scheme` using `salt`.
|
Hash ``password`` with ``scheme`` using ``salt``.
|
||||||
This three variable beeing encoded in `charset`.
|
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()
|
scheme = scheme.upper()
|
||||||
cls._test_scheme(scheme)
|
cls._test_scheme(scheme)
|
||||||
@ -339,7 +513,14 @@ class LdapHashUserPassword(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_scheme(cls, hashed_passord):
|
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:
|
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)
|
raise cls.BadHash("%r should start with the scheme enclosed with { }" % hashed_passord)
|
||||||
scheme = hashed_passord.split(b'}', 1)[0]
|
scheme = hashed_passord.split(b'}', 1)[0]
|
||||||
@ -348,7 +529,15 @@ class LdapHashUserPassword(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_salt(cls, hashed_passord):
|
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)
|
scheme = cls.get_scheme(hashed_passord)
|
||||||
cls._test_scheme(scheme)
|
cls._test_scheme(scheme)
|
||||||
if scheme in cls.schemes_nosalt:
|
if scheme in cls.schemes_nosalt:
|
||||||
@ -364,8 +553,20 @@ class LdapHashUserPassword(object):
|
|||||||
|
|
||||||
def check_password(method, password, hashed_password, charset):
|
def check_password(method, password, hashed_password, charset):
|
||||||
"""
|
"""
|
||||||
Check that `password` match `hashed_password` using `method`,
|
Check that ``password`` match `hashed_password` using ``method``,
|
||||||
assuming the encoding is `charset`.
|
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):
|
if not isinstance(password, six.binary_type):
|
||||||
password = password.encode(charset)
|
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
|
pytest-cov>=2.2.1
|
||||||
requests>=2.4
|
requests>=2.4
|
||||||
requests_futures>=0.9.5
|
requests_futures>=0.9.5
|
||||||
django-picklefield>=0.3.1
|
|
||||||
django-bootstrap3>=5.4
|
|
||||||
lxml>=3.4
|
lxml>=3.4
|
||||||
six>=1
|
six>=1
|
||||||
|
@ -2,7 +2,5 @@ Django >= 1.8,<1.10
|
|||||||
setuptools>=5.5
|
setuptools>=5.5
|
||||||
requests>=2.4
|
requests>=2.4
|
||||||
requests_futures>=0.9.5
|
requests_futures>=0.9.5
|
||||||
django-picklefield>=0.3.1
|
|
||||||
django-bootstrap3>=5.4
|
|
||||||
lxml>=3.4
|
lxml>=3.4
|
||||||
six>=1
|
six>=1
|
||||||
|
11
setup.cfg
11
setup.cfg
@ -1,2 +1,13 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
description-file = README.rst
|
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
|
import pkg_resources
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
VERSION = '0.6.0'
|
||||||
|
|
||||||
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
|
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
|
||||||
README = readme.read()
|
README = readme.read()
|
||||||
|
|
||||||
# allow setup.py to be run from any path
|
if __name__ == '__main__':
|
||||||
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
# allow setup.py to be run from any path
|
||||||
|
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
||||||
|
|
||||||
|
setup(
|
||||||
# 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(
|
|
||||||
name='django-cas-server',
|
name='django-cas-server',
|
||||||
version='0.6.0',
|
version=VERSION,
|
||||||
packages=[
|
packages=[
|
||||||
'cas_server', 'cas_server.migrations',
|
'cas_server', 'cas_server.migrations',
|
||||||
'cas_server.management', 'cas_server.management.commands',
|
'cas_server.management', 'cas_server.management.commands',
|
||||||
@ -48,15 +30,25 @@ setup(
|
|||||||
author_email='valentin.samir@crans.org',
|
author_email='valentin.samir@crans.org',
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
|
'evelopment Status :: 5 - Production/Stable',
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
|
'Framework :: Django :: 1.7',
|
||||||
|
'Framework :: Django :: 1.8',
|
||||||
|
'Framework :: Django :: 1.9',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
'Intended Audience :: System Administrators',
|
||||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 2',
|
'Programming Language :: Python :: 2',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3',
|
'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',
|
||||||
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||||
|
'Topic :: System :: Systems Administration :: Authentication/Directory'
|
||||||
],
|
],
|
||||||
package_data={
|
package_data={
|
||||||
'cas_server': [
|
'cas_server': [
|
||||||
@ -67,11 +59,12 @@ setup(
|
|||||||
},
|
},
|
||||||
keywords=['django', 'cas', 'cas3', 'server', 'sso', 'single sign-on', 'authentication', 'auth'],
|
keywords=['django', 'cas', 'cas3', 'server', 'sso', 'single sign-on', 'authentication', 'auth'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
django, 'requests >= 2.4', 'requests_futures >= 0.9.5',
|
'Django >= 1.7,<1.10', 'requests >= 2.4', 'requests_futures >= 0.9.5',
|
||||||
'django-picklefield >= 0.3.1', django_bootstrap3, 'lxml >= 3.4',
|
'lxml >= 3.4', 'six >= 1'
|
||||||
'six >= 1'
|
|
||||||
],
|
],
|
||||||
url="https://github.com/nitmir/django-cas-server",
|
url="https://github.com/nitmir/django-cas-server",
|
||||||
download_url="https://github.com/nitmir/django-cas-server/releases",
|
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-django17,
|
||||||
py34-django18,
|
py34-django18,
|
||||||
py34-django19,
|
py34-django19,
|
||||||
|
py35-django18,
|
||||||
|
py35-django19,
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length=100
|
max-line-length=100
|
||||||
@ -69,6 +71,19 @@ deps =
|
|||||||
Django>=1.9,<1.10
|
Django>=1.9,<1.10
|
||||||
{[base]deps}
|
{[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]
|
[testenv:flake8]
|
||||||
basepython=python
|
basepython=python
|
||||||
deps=flake8
|
deps=flake8
|
||||||
|
Loading…
Reference in New Issue
Block a user