commit
ba59060763
@ -1,8 +1,6 @@
|
|||||||
language: python
|
language: python
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- python: "2.7"
|
|
||||||
env: TOX_ENV=coverage
|
|
||||||
- python: "2.7"
|
- python: "2.7"
|
||||||
env: TOX_ENV=flake8
|
env: TOX_ENV=flake8
|
||||||
- python: "2.7"
|
- python: "2.7"
|
||||||
@ -23,6 +21,8 @@ matrix:
|
|||||||
env: TOX_ENV=py35-django18
|
env: TOX_ENV=py35-django18
|
||||||
- python: "3.5"
|
- python: "3.5"
|
||||||
env: TOX_ENV=py35-django19
|
env: TOX_ENV=py35-django19
|
||||||
|
- python: "2.7"
|
||||||
|
env: TOX_ENV=coverage
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.cache/pip/http/
|
- $HOME/.cache/pip/http/
|
||||||
|
4
Makefile
4
Makefile
@ -38,7 +38,7 @@ dist:
|
|||||||
|
|
||||||
test_venv/bin/python:
|
test_venv/bin/python:
|
||||||
virtualenv test_venv
|
virtualenv test_venv
|
||||||
test_venv/bin/pip install -U --requirement requirements-dev.txt Django
|
test_venv/bin/pip install -U --requirement requirements-dev.txt 'Django<1.10'
|
||||||
|
|
||||||
test_venv/cas/manage.py: test_venv
|
test_venv/cas/manage.py: test_venv
|
||||||
mkdir -p test_venv/cas
|
mkdir -p test_venv/cas
|
||||||
@ -62,7 +62,7 @@ run_server: test_project
|
|||||||
|
|
||||||
run_tests: test_venv
|
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 -rw -x --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/sphinx-build: test_venv
|
||||||
|
142
README.rst
142
README.rst
@ -29,11 +29,49 @@ Dependencies
|
|||||||
|
|
||||||
``django-cas-server`` depends on the following python packages:
|
``django-cas-server`` depends on the following python packages:
|
||||||
|
|
||||||
* Django >= 1.7 < 1.10
|
* Django >= 1.7.1 < 1.10
|
||||||
* requests >= 2.4
|
* requests >= 2.4
|
||||||
* requests_futures >= 0.9.5
|
* requests_futures >= 0.9.5
|
||||||
* lxml >= 3.4
|
* lxml >= 3.4
|
||||||
* six >= 1
|
* six >= 1.8
|
||||||
|
|
||||||
|
Minimal version of packages dependancy are just indicative and meens that ``django-cas-server`` has
|
||||||
|
been tested with it. Previous versions of dependencies may or may not work.
|
||||||
|
|
||||||
|
Additionally, denpending of the authentication backend you plan to use, you may need the following
|
||||||
|
python packages:
|
||||||
|
|
||||||
|
* ldap3
|
||||||
|
* psycopg2
|
||||||
|
* mysql-python
|
||||||
|
|
||||||
|
|
||||||
|
Here there is a table with the name of python packages and the corresponding packages providing
|
||||||
|
them on debian like systems and centos like systems.
|
||||||
|
You should try as much as possible to use system packages as there are automatically updated then
|
||||||
|
you update your system. You can then install Not Available (N/A)
|
||||||
|
packages on your system using pip inside a virtualenv as described in the `Installation`_ section.
|
||||||
|
For use with python3, just replace python(2) in the table by python3.
|
||||||
|
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
| python package | debian like systems | centos like systems |
|
||||||
|
+==================+=========================+=====================+
|
||||||
|
| Django | python-django | python-django |
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
| requests | python-requests | python-requests |
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
| requests_futures | python-requests-futures | N/A |
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
| lxml | python-lxml | python-lxml |
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
| six | python-six | python-six |
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
| ldap3 | python-ldap3 | python-ldap3 |
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
| psycopg2 | python-psycopg2 | python-psycopg2 |
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
| mysql-python | python-mysqldb | python2-mysql |
|
||||||
|
+------------------+-------------------------+---------------------+
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
============
|
============
|
||||||
@ -63,14 +101,17 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
|
|||||||
New python executable in cas/bin/python2
|
New python executable in cas/bin/python2
|
||||||
Also creating executable in cas/bin/python
|
Also creating executable in cas/bin/python
|
||||||
Installing setuptools, pip...done.
|
Installing setuptools, pip...done.
|
||||||
|
|
||||||
|
4. And `activate it <https://virtualenv.pypa.io/en/stable/userguide/#activate-script>`__::
|
||||||
|
|
||||||
$ cd cas_venv/; . bin/activate
|
$ cd cas_venv/; . bin/activate
|
||||||
|
|
||||||
4. Create a django project::
|
5. Create a django project::
|
||||||
|
|
||||||
$ django-admin startproject cas_project
|
$ django-admin startproject cas_project
|
||||||
$ cd cas_project
|
$ cd cas_project
|
||||||
|
|
||||||
5. Install `django-cas-server`. To use the last published release, run::
|
6. Install `django-cas-server`. To use the last published release, run::
|
||||||
|
|
||||||
$ pip install django-cas-server
|
$ pip install django-cas-server
|
||||||
|
|
||||||
@ -81,11 +122,11 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
|
|||||||
$ pip install -r requirements.txt
|
$ pip install -r requirements.txt
|
||||||
|
|
||||||
Then, either run ``make install`` to create a python package using the sources of the repository
|
Then, either run ``make install`` to create a python package using the sources of the repository
|
||||||
and install it with pip, or place the `cas_server` directory into your
|
and install it with pip, or place the ``cas_server`` directory into your
|
||||||
`PYTHONPATH <https://docs.python.org/2/using/cmdline.html#envvar-PYTHONPATH>`_
|
`PYTHONPATH <https://docs.python.org/2/using/cmdline.html#envvar-PYTHONPATH>`_
|
||||||
(for instance by symlinking `cas_server` to the root of your django project).
|
(for instance by symlinking ``cas_server`` to the root of your django project).
|
||||||
|
|
||||||
6. Open ``cas_project/settings.py`` in you favourite editor and follow the quick start section.
|
7. Open ``cas_project/settings.py`` in you favourite editor and follow the quick start section.
|
||||||
|
|
||||||
|
|
||||||
Quick start
|
Quick start
|
||||||
@ -145,7 +186,7 @@ Quick start
|
|||||||
|
|
||||||
6. Start the development server and visit http://127.0.0.1:8000/admin/
|
6. Start the development server and visit http://127.0.0.1:8000/admin/
|
||||||
to add a first service allowed to authenticate user against the CAS
|
to add a first service allowed to authenticate user against the CAS
|
||||||
(you'll need the Admin app enabled). See the Service Patterns section bellow.
|
(you'll need the Admin app enabled). See the `Service Patterns`_ section bellow.
|
||||||
|
|
||||||
7. Visit http://127.0.0.1:8000/cas/ to login with your django users.
|
7. Visit http://127.0.0.1:8000/cas/ to login with your django users.
|
||||||
|
|
||||||
@ -163,6 +204,8 @@ 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_FAVICON_URL``: URL to the favicon (shortcut icon) used by the default templates.
|
||||||
|
Default is a key icon. Set it to ``False`` to disable it.
|
||||||
* ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary
|
* ``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"``,
|
and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
|
||||||
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
|
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
|
||||||
@ -193,6 +236,8 @@ Authentication settings
|
|||||||
|
|
||||||
* ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing
|
* ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing
|
||||||
``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"``
|
``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"``
|
||||||
|
Available classes bundled with ``django-cas-server`` are listed below in the
|
||||||
|
`Authentication backend`_ section.
|
||||||
|
|
||||||
* ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after
|
* ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after
|
||||||
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
|
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
|
||||||
@ -212,13 +257,23 @@ Authentication settings
|
|||||||
Federation settings
|
Federation settings
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
* ``CAS_FEDERATE``: A boolean for activating the federated mode (see the federate section below).
|
* ``CAS_FEDERATE``: A boolean for activating the federated mode (see the `Federation mode`_
|
||||||
The default is ``False``.
|
section below). The default is ``False``.
|
||||||
* ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity
|
* ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity
|
||||||
provider" expire. The default is ``604800``, one week. The cookie is called
|
provider" expire. The default is ``604800``, one week. The cookie is called
|
||||||
``_remember_provider``.
|
``_remember_provider``.
|
||||||
|
|
||||||
|
|
||||||
|
New version warnings settings
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
* ``CAS_NEW_VERSION_HTML_WARNING``: A boolean for diplaying a warning on html pages then a new
|
||||||
|
version of the application is avaible. Once closed by a user, it is not displayed to this user
|
||||||
|
until the next new version. The default is ``True``.
|
||||||
|
* ``CAS_NEW_VERSION_EMAIL_WARNING``: A bolean sot sending a email to ``settings.ADMINS`` when a new
|
||||||
|
version is available. The default is ``True``.
|
||||||
|
|
||||||
|
|
||||||
Tickets validity settings
|
Tickets validity settings
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
@ -257,6 +312,7 @@ Tickets miscellaneous settings
|
|||||||
|
|
||||||
Mysql backend settings
|
Mysql backend settings
|
||||||
----------------------
|
----------------------
|
||||||
|
Deprecated, see the `Sql backend settings`_.
|
||||||
Only usefull if you are using the mysql authentication backend:
|
Only usefull if you are using the mysql authentication backend:
|
||||||
|
|
||||||
* ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``.
|
* ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``.
|
||||||
@ -283,6 +339,64 @@ Only usefull if you are using the mysql authentication backend:
|
|||||||
The default is ``"crypt"``.
|
The default is ``"crypt"``.
|
||||||
|
|
||||||
|
|
||||||
|
Sql backend settings
|
||||||
|
--------------------
|
||||||
|
Only usefull if you are using the sql authentication backend. You must add a ``"cas_server"``
|
||||||
|
database to `settings.DATABASES <https://docs.djangoproject.com/fr/1.9/ref/settings/#std:setting-DATABASES>`__
|
||||||
|
as defined in the django documentation. It is then the database
|
||||||
|
use by the sql backend.
|
||||||
|
|
||||||
|
* ``CAS_SQL_USER_QUERY``: The query performed upon user authentication.
|
||||||
|
The username must be in field ``username``, the password in ``password``,
|
||||||
|
additional fields are used as the user attributes.
|
||||||
|
The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"``
|
||||||
|
* ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following:
|
||||||
|
|
||||||
|
* ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database
|
||||||
|
should begin this $
|
||||||
|
* ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html)
|
||||||
|
the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256},
|
||||||
|
{SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}.
|
||||||
|
* ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512.
|
||||||
|
The hashed password in the database is compare to the hexadecimal digest of the clear
|
||||||
|
password hashed with the corresponding algorithm.
|
||||||
|
* ``"plain"``, the password in the database must be in clear.
|
||||||
|
|
||||||
|
The default is ``"crypt"``.
|
||||||
|
* ``CAS_SQL_PASSWORD_CHARSET``: Charset the SQL users passwords was hash with. This is needed to
|
||||||
|
encode the user sended password before hashing it for comparison. The default is ``"utf-8"``.
|
||||||
|
|
||||||
|
|
||||||
|
Ldap backend settings
|
||||||
|
---------------------
|
||||||
|
Only usefull if you are using the ldap authentication backend:
|
||||||
|
|
||||||
|
* ``CAS_LDAP_SERVER``: Address of the LDAP server. The default is ``"localhost"``.
|
||||||
|
* ``CAS_LDAP_USER``: User bind address, for example ``"cn=admin,dc=crans,dc=org"`` for
|
||||||
|
connecting to the LDAP server.
|
||||||
|
* ``CAS_LDAP_PASSWORD``: Password for connecting to the LDAP server.
|
||||||
|
* ``CAS_LDAP_BASE_DN``: LDAP search base DN, for example ``"ou=data,dc=crans,dc=org"``.
|
||||||
|
* ``CAS_LDAP_USER_QUERY``: Search filter for searching user by username. User inputed usernames are
|
||||||
|
escaped using ``ldap3.utils.conv.escape_bytes``. The default is ``"(uid=%s)"``
|
||||||
|
* ``CAS_LDAP_USERNAME_ATTR``: Attribute used for users usernames. The default is ``"uid"``
|
||||||
|
* ``CAS_LDAP_PASSWORD_ATTR``: Attribute used for users passwords. The default is ``"userPassword"``
|
||||||
|
* ``CAS_LDAP_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following:
|
||||||
|
|
||||||
|
* ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database
|
||||||
|
should begin this $
|
||||||
|
* ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html)
|
||||||
|
the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256},
|
||||||
|
{SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}.
|
||||||
|
* ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512.
|
||||||
|
The hashed password in the database is compare to the hexadecimal digest of the clear
|
||||||
|
password hashed with the corresponding algorithm.
|
||||||
|
* ``"plain"``, the password in the database must be in clear.
|
||||||
|
|
||||||
|
The default is ``"ldap"``.
|
||||||
|
* ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to
|
||||||
|
encode the user sended password before hashing it for comparison. The default is ``"utf-8"``.
|
||||||
|
|
||||||
|
|
||||||
Test backend settings
|
Test backend settings
|
||||||
---------------------
|
---------------------
|
||||||
Only usefull if you are using the test authentication backend:
|
Only usefull if you are using the test authentication backend:
|
||||||
@ -304,11 +418,17 @@ Authentication backend
|
|||||||
for the user are defined by the ``CAS_TEST_*`` settings.
|
for the user are defined by the ``CAS_TEST_*`` settings.
|
||||||
* django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system.
|
* django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system.
|
||||||
This is the default backend. The returned attributes are the fields available on the user model.
|
This is the default backend. The returned attributes are the fields available on the user model.
|
||||||
* mysql backend ``cas_server.auth.MysqlAuthUser``: see the 'Mysql backend settings' section.
|
* mysql backend ``cas_server.auth.MysqlAuthUser``: Deprecated, use the sql backend instead.
|
||||||
|
see the `Mysql backend settings`_ section. The returned attributes are those return by sql query
|
||||||
|
``CAS_SQL_USER_QUERY``.
|
||||||
|
* sql backend ``cas_server.auth.SqlAuthUser``: see the `Sql backend settings`_ section.
|
||||||
The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``.
|
The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``.
|
||||||
|
* ldap backend ``cas_server.auth.LdapAuthUser``: see the `Ldap backend settings`_ section.
|
||||||
|
The returned attributes are those of the ldap node returned by the query filter ``CAS_LDAP_USER_QUERY``.
|
||||||
* federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``.
|
* federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``.
|
||||||
You should not set it manually without setting ``CAS_FEDERATE`` to ``True``.
|
You should not set it manually without setting ``CAS_FEDERATE`` to ``True``.
|
||||||
|
|
||||||
|
|
||||||
Logs
|
Logs
|
||||||
====
|
====
|
||||||
|
|
||||||
|
@ -9,5 +9,9 @@
|
|||||||
#
|
#
|
||||||
# (c) 2015-2016 Valentin Samir
|
# (c) 2015-2016 Valentin Samir
|
||||||
"""A django CAS server application"""
|
"""A django CAS server application"""
|
||||||
|
|
||||||
|
#: version of the application
|
||||||
|
VERSION = '0.6.2'
|
||||||
|
|
||||||
#: path the the application configuration class
|
#: path the the application configuration class
|
||||||
default_app_config = 'cas_server.apps.CasAppConfig'
|
default_app_config = 'cas_server.apps.CasAppConfig'
|
||||||
|
@ -13,16 +13,25 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.db import connections, DatabaseError
|
||||||
|
|
||||||
|
import warnings
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from six.moves import range
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
import MySQLdb
|
import MySQLdb
|
||||||
import MySQLdb.cursors
|
import MySQLdb.cursors
|
||||||
from utils import check_password
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
MySQLdb = None
|
MySQLdb = None
|
||||||
|
|
||||||
|
|
||||||
|
try: # pragma: no cover
|
||||||
|
import ldap3
|
||||||
|
except ImportError:
|
||||||
|
ldap3 = None
|
||||||
|
|
||||||
from .models import FederatedUser
|
from .models import FederatedUser
|
||||||
|
from .utils import check_password, dictfetchall
|
||||||
|
|
||||||
|
|
||||||
class AuthUser(object):
|
class AuthUser(object):
|
||||||
@ -116,19 +125,46 @@ class TestAuthUser(AuthUser):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class MysqlAuthUser(AuthUser): # pragma: no cover
|
class DBAuthUser(AuthUser): # pragma: no cover
|
||||||
|
"""base class for databate based auth classes"""
|
||||||
|
#: DB user attributes as a :class:`dict` if the username is found in the database.
|
||||||
|
user = None
|
||||||
|
|
||||||
|
def attributs(self):
|
||||||
"""
|
"""
|
||||||
A mysql authentication class: authentication user agains a mysql database
|
The user attributes.
|
||||||
|
|
||||||
|
:return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode`
|
||||||
|
or :class:`list` of :func:`unicode`. If the user do not exists, the returned
|
||||||
|
:class:`dict` is empty.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
if self.user:
|
||||||
|
return self.user
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class MysqlAuthUser(DBAuthUser): # pragma: no cover
|
||||||
|
"""
|
||||||
|
DEPRECATED, use :class:`SqlAuthUser` instead.
|
||||||
|
|
||||||
|
A mysql authentication class: authenticate user agains a mysql database
|
||||||
|
|
||||||
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||||
class attribute. Valid value are fetched from the MySQL database set with
|
class attribute. Valid value are fetched from the MySQL database set with
|
||||||
``settings.CAS_SQL_*`` settings parameters using the query
|
``settings.CAS_SQL_*`` settings parameters using the query
|
||||||
``settings.CAS_SQL_USER_QUERY``.
|
``settings.CAS_SQL_USER_QUERY``.
|
||||||
"""
|
"""
|
||||||
#: Mysql user attributes as a :class:`dict` if the username is found in the database.
|
|
||||||
user = None
|
|
||||||
|
|
||||||
def __init__(self, username):
|
def __init__(self, username):
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"MysqlAuthUser authentication class is deprecated: "
|
||||||
|
"use cas_server.auth.SqlAuthUser instead"
|
||||||
|
),
|
||||||
|
UserWarning
|
||||||
|
)
|
||||||
# see the connect function at
|
# see the connect function at
|
||||||
# http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes
|
# http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes
|
||||||
# for possible mysql config parameters.
|
# for possible mysql config parameters.
|
||||||
@ -169,24 +205,130 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def attributs(self):
|
|
||||||
"""
|
|
||||||
The user attributes.
|
|
||||||
|
|
||||||
:return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode`
|
class SqlAuthUser(DBAuthUser): # pragma: no cover
|
||||||
or :class:`list` of :func:`unicode`. If the user do not exists, the returned
|
"""
|
||||||
:class:`dict` is empty.
|
A SQL authentication class: authenticate user agains a SQL database. The SQL database
|
||||||
:rtype: dict
|
must be configures in settings.py as ``settings.DATABASES['cas_server']``.
|
||||||
|
|
||||||
|
: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``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, username):
|
||||||
|
if "cas_server" not in connections:
|
||||||
|
raise RuntimeError("Please configure the 'cas_server' database in settings.DATABASES")
|
||||||
|
for retry_nb in range(3):
|
||||||
|
try:
|
||||||
|
with connections["cas_server"].cursor() as curs:
|
||||||
|
curs.execute(settings.CAS_SQL_USER_QUERY, (username,))
|
||||||
|
results = dictfetchall(curs)
|
||||||
|
if len(results) == 1:
|
||||||
|
self.user = results[0]
|
||||||
|
super(SqlAuthUser, self).__init__(self.user['username'])
|
||||||
|
else:
|
||||||
|
super(SqlAuthUser, self).__init__(username)
|
||||||
|
break
|
||||||
|
except DatabaseError:
|
||||||
|
connections["cas_server"].close()
|
||||||
|
if retry_nb == 2:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def test_password(self, password):
|
||||||
|
"""
|
||||||
|
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 self.user
|
return check_password(
|
||||||
|
settings.CAS_SQL_PASSWORD_CHECK,
|
||||||
|
password,
|
||||||
|
self.user["password"],
|
||||||
|
settings.CAS_SQL_PASSWORD_CHARSET
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return {}
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class LdapAuthUser(DBAuthUser): # pragma: no cover
|
||||||
|
"""
|
||||||
|
A ldap authentication class: authenticate user against a ldap database
|
||||||
|
|
||||||
|
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||||
|
class attribute. Valid value are fetched from the ldap database set with
|
||||||
|
``settings.CAS_LDAP_*`` settings parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_conn = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_conn(cls):
|
||||||
|
"""Return a connection object to the ldap database"""
|
||||||
|
conn = cls._conn
|
||||||
|
if conn is None or conn.closed:
|
||||||
|
conn = ldap3.Connection(
|
||||||
|
settings.CAS_LDAP_SERVER,
|
||||||
|
settings.CAS_LDAP_USER,
|
||||||
|
settings.CAS_LDAP_PASSWORD,
|
||||||
|
auto_bind=True
|
||||||
|
)
|
||||||
|
cls._conn = conn
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def __init__(self, username):
|
||||||
|
if not ldap3:
|
||||||
|
raise RuntimeError("Please install ldap3 before using the LdapAuthUser backend")
|
||||||
|
# in case we got deconnected from the database, retry to connect 2 times
|
||||||
|
for retry_nb in range(3):
|
||||||
|
try:
|
||||||
|
conn = self.get_conn()
|
||||||
|
if conn.search(
|
||||||
|
settings.CAS_LDAP_BASE_DN,
|
||||||
|
settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(username),
|
||||||
|
attributes=ldap3.ALL_ATTRIBUTES
|
||||||
|
) and len(conn.entries) == 1:
|
||||||
|
user = conn.entries[0].entry_get_attributes_dict()
|
||||||
|
if user.get(settings.CAS_LDAP_USERNAME_ATTR):
|
||||||
|
self.user = user
|
||||||
|
super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0])
|
||||||
|
else:
|
||||||
|
super(LdapAuthUser, self).__init__(username)
|
||||||
|
else:
|
||||||
|
super(LdapAuthUser, self).__init__(username)
|
||||||
|
break
|
||||||
|
except ldap3.LDAPCommunicationError:
|
||||||
|
if retry_nb == 2:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def test_password(self, password):
|
||||||
|
"""
|
||||||
|
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 and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR):
|
||||||
|
return check_password(
|
||||||
|
settings.CAS_LDAP_PASSWORD_CHECK,
|
||||||
|
password,
|
||||||
|
self.user[settings.CAS_LDAP_PASSWORD_ATTR][0],
|
||||||
|
settings.CAS_LDAP_PASSWORD_CHARSET
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
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 against django internal users
|
||||||
|
|
||||||
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
|
||||||
class attribute. Valid value are usernames of django internal users.
|
class attribute. Valid value are usernames of django internal users.
|
||||||
|
@ -134,6 +134,14 @@ class CASClientBase(object):
|
|||||||
raise CASError(errors[0].attrib['code'], errors[0].text)
|
raise CASError(errors[0].attrib['code'], errors[0].text)
|
||||||
raise CASError("Bad http code %s" % response.code)
|
raise CASError("Bad http code %s" % response.code)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_page_charset(page, default="utf-8"):
|
||||||
|
content_type = page.info().get('Content-type')
|
||||||
|
if content_type and "charset=" in content_type:
|
||||||
|
return content_type.split("charset=")[-1]
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
class CASClientV1(CASClientBase, ReturnUnicode):
|
class CASClientV1(CASClientBase, ReturnUnicode):
|
||||||
"""CAS Client Version 1"""
|
"""CAS Client Version 1"""
|
||||||
@ -146,17 +154,15 @@ class CASClientV1(CASClientBase, ReturnUnicode):
|
|||||||
Returns username on success and None on failure.
|
Returns username on success and None on failure.
|
||||||
"""
|
"""
|
||||||
params = [('ticket', ticket), ('service', self.service_url)]
|
params = [('ticket', ticket), ('service', self.service_url)]
|
||||||
|
if self.renew:
|
||||||
|
params.append(('renew', 'true'))
|
||||||
url = (urllib_parse.urljoin(self.server_url, 'validate') + '?' +
|
url = (urllib_parse.urljoin(self.server_url, 'validate') + '?' +
|
||||||
urllib_parse.urlencode(params))
|
urllib_parse.urlencode(params))
|
||||||
page = urllib_request.urlopen(url)
|
page = urllib_request.urlopen(url)
|
||||||
try:
|
try:
|
||||||
verified = page.readline().strip()
|
verified = page.readline().strip()
|
||||||
if verified == b'yes':
|
if verified == b'yes':
|
||||||
content_type = page.info().get('Content-type')
|
charset = self.get_page_charset(page, default="ascii")
|
||||||
if "charset=" in content_type:
|
|
||||||
charset = content_type.split("charset=")[-1]
|
|
||||||
else:
|
|
||||||
charset = "ascii"
|
|
||||||
user = self.u(page.readline().strip(), charset)
|
user = self.u(page.readline().strip(), charset)
|
||||||
return user, None, None
|
return user, None, None
|
||||||
else:
|
else:
|
||||||
@ -183,17 +189,15 @@ class CASClientV2(CASClientBase, ReturnUnicode):
|
|||||||
|
|
||||||
def get_verification_response(self, ticket):
|
def get_verification_response(self, ticket):
|
||||||
params = [('ticket', ticket), ('service', self.service_url)]
|
params = [('ticket', ticket), ('service', self.service_url)]
|
||||||
|
if self.renew:
|
||||||
|
params.append(('renew', 'true'))
|
||||||
if self.proxy_callback:
|
if self.proxy_callback:
|
||||||
params.append(('pgtUrl', self.proxy_callback))
|
params.append(('pgtUrl', self.proxy_callback))
|
||||||
base_url = urllib_parse.urljoin(self.server_url, self.url_suffix)
|
base_url = urllib_parse.urljoin(self.server_url, self.url_suffix)
|
||||||
url = base_url + '?' + urllib_parse.urlencode(params)
|
url = base_url + '?' + urllib_parse.urlencode(params)
|
||||||
page = urllib_request.urlopen(url)
|
page = urllib_request.urlopen(url)
|
||||||
try:
|
try:
|
||||||
content_type = page.info().get('Content-type')
|
charset = self.get_page_charset(page)
|
||||||
if "charset=" in content_type:
|
|
||||||
charset = content_type.split("charset=")[-1]
|
|
||||||
else:
|
|
||||||
charset = "ascii"
|
|
||||||
return (page.read(), charset)
|
return (page.read(), charset)
|
||||||
finally:
|
finally:
|
||||||
page.close()
|
page.close()
|
||||||
@ -306,11 +310,7 @@ class CASClientWithSAMLV1(CASClientV2, SingleLogoutMixin):
|
|||||||
from elementtree import ElementTree
|
from elementtree import ElementTree
|
||||||
|
|
||||||
page = self.fetch_saml_validation(ticket)
|
page = self.fetch_saml_validation(ticket)
|
||||||
content_type = page.info().get('Content-type')
|
charset = self.get_page_charset(page)
|
||||||
if "charset=" in content_type:
|
|
||||||
charset = content_type.split("charset=")[-1]
|
|
||||||
else:
|
|
||||||
charset = "ascii"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = None
|
user = None
|
||||||
|
@ -18,6 +18,8 @@ from importlib import import_module
|
|||||||
|
|
||||||
#: URL to the logo showed in the up left corner on the default templates.
|
#: URL to the logo showed in the up left corner on the default templates.
|
||||||
CAS_LOGO_URL = static("cas_server/logo.png")
|
CAS_LOGO_URL = static("cas_server/logo.png")
|
||||||
|
#: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon.
|
||||||
|
CAS_FAVICON_URL = static("cas_server/favicon.ico")
|
||||||
#: URLs to css and javascript external components.
|
#: URLs to css and javascript external components.
|
||||||
CAS_COMPONENT_URLS = {
|
CAS_COMPONENT_URLS = {
|
||||||
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
|
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
|
||||||
@ -110,12 +112,39 @@ CAS_SQL_PASSWORD = ''
|
|||||||
CAS_SQL_DBNAME = ''
|
CAS_SQL_DBNAME = ''
|
||||||
#: Database charset.
|
#: Database charset.
|
||||||
CAS_SQL_DBCHARSET = 'utf8'
|
CAS_SQL_DBCHARSET = 'utf8'
|
||||||
|
|
||||||
#: The query performed upon user authentication.
|
#: The query performed upon user authentication.
|
||||||
CAS_SQL_USER_QUERY = 'SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s'
|
CAS_SQL_USER_QUERY = 'SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s'
|
||||||
#: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``,
|
#: 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_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``,
|
||||||
#: ``"hex_sha512"``, ``"plain"``.
|
#: ``"hex_sha512"``, ``"plain"``.
|
||||||
CAS_SQL_PASSWORD_CHECK = 'crypt' # crypt or plain
|
CAS_SQL_PASSWORD_CHECK = 'crypt'
|
||||||
|
#: charset the SQL users passwords was hash with
|
||||||
|
CAS_SQL_PASSWORD_CHARSET = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
#: Address of the LDAP server
|
||||||
|
CAS_LDAP_SERVER = 'localhost'
|
||||||
|
#: LDAP user bind address, for example ``"cn=admin,dc=crans,dc=org"`` for connecting to the LDAP
|
||||||
|
#: server.
|
||||||
|
CAS_LDAP_USER = None
|
||||||
|
#: LDAP connection password
|
||||||
|
CAS_LDAP_PASSWORD = None
|
||||||
|
#: LDAP seach base DN, for example ``"ou=data,dc=crans,dc=org"``.
|
||||||
|
CAS_LDAP_BASE_DN = None
|
||||||
|
#: LDAP search filter for searching user by username. User inputed usernames are escaped using
|
||||||
|
#: :func:`ldap3.utils.conv.escape_bytes`.
|
||||||
|
CAS_LDAP_USER_QUERY = "(uid=%s)"
|
||||||
|
#: LDAP attribute used for users usernames
|
||||||
|
CAS_LDAP_USERNAME_ATTR = "uid"
|
||||||
|
#: LDAP attribute used for users passwords
|
||||||
|
CAS_LDAP_PASSWORD_ATTR = "userPassword"
|
||||||
|
#: 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_LDAP_PASSWORD_CHECK = "ldap"
|
||||||
|
#: charset the LDAP users passwords was hash with
|
||||||
|
CAS_LDAP_PASSWORD_CHARSET = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
#: Username of the test user.
|
#: Username of the test user.
|
||||||
@ -140,6 +169,15 @@ CAS_FEDERATE = False
|
|||||||
#: Time after witch the cookie use for “remember my identity provider” expire (one week).
|
#: Time after witch the cookie use for “remember my identity provider” expire (one week).
|
||||||
CAS_FEDERATE_REMEMBER_TIMEOUT = 604800
|
CAS_FEDERATE_REMEMBER_TIMEOUT = 604800
|
||||||
|
|
||||||
|
#: A :class:`bool` for diplaying a warning on html pages then a new version of the application
|
||||||
|
#: is avaible. Once closed by a user, it is not displayed to this user until the next new version.
|
||||||
|
CAS_NEW_VERSION_HTML_WARNING = True
|
||||||
|
#: A :class:`bool` for sending emails to ``settings.ADMINS`` when a new version is available.
|
||||||
|
CAS_NEW_VERSION_EMAIL_WARNING = True
|
||||||
|
#: URL to the pypi json of the application. Used to retreive the version number of the last version.
|
||||||
|
#: You should not change it.
|
||||||
|
CAS_NEW_VERSION_JSON_URL = "https://pypi.python.org/pypi/django-cas-server/json"
|
||||||
|
|
||||||
GLOBALS = globals().copy()
|
GLOBALS = globals().copy()
|
||||||
for name, default_value in GLOBALS.items():
|
for name, default_value in GLOBALS.items():
|
||||||
# get the current setting value, falling back to default_value
|
# get the current setting value, falling back to default_value
|
||||||
|
@ -42,13 +42,13 @@ class CASFederateValidateUser(object):
|
|||||||
#: the identity provider
|
#: the identity provider
|
||||||
provider = None
|
provider = None
|
||||||
|
|
||||||
def __init__(self, provider, service_url):
|
def __init__(self, provider, service_url, renew=False):
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
self.client = CASClient(
|
self.client = CASClient(
|
||||||
service_url=service_url,
|
service_url=service_url,
|
||||||
version=provider.cas_protocol_version,
|
version=provider.cas_protocol_version,
|
||||||
server_url=provider.server_url,
|
server_url=provider.server_url,
|
||||||
renew=False,
|
renew=renew,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_login_url(self):
|
def get_login_url(self):
|
||||||
|
@ -19,49 +19,56 @@ import cas_server.models as models
|
|||||||
|
|
||||||
|
|
||||||
class BootsrapForm(forms.Form):
|
class BootsrapForm(forms.Form):
|
||||||
"""Form base class to use boostrap then rendering the form fields"""
|
"""
|
||||||
|
Bases: :class:`django.forms.Form`
|
||||||
|
|
||||||
|
Form base class to use boostrap then rendering the form fields
|
||||||
|
"""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(BootsrapForm, self).__init__(*args, **kwargs)
|
super(BootsrapForm, self).__init__(*args, **kwargs)
|
||||||
for (name, field) in self.fields.items():
|
for field in self.fields.values():
|
||||||
# Only tweak the fiel if it will be displayed
|
# Only tweak the fiel if it will be displayed
|
||||||
if not isinstance(field.widget, forms.HiddenInput):
|
if not isinstance(field.widget, forms.HiddenInput):
|
||||||
# tell to display the field (used in form.html)
|
|
||||||
self[name].display = True
|
|
||||||
attrs = {}
|
attrs = {}
|
||||||
if isinstance(field.widget, forms.CheckboxInput):
|
if not isinstance(field.widget, forms.CheckboxInput):
|
||||||
self[name].checkbox = True
|
|
||||||
else:
|
|
||||||
attrs['class'] = "form-control"
|
attrs['class'] = "form-control"
|
||||||
if field.label:
|
if field.label: # pragma: no branch (currently all field are hidden or labeled)
|
||||||
attrs["placeholder"] = field.label
|
attrs["placeholder"] = field.label
|
||||||
if field.required:
|
if field.required:
|
||||||
attrs["required"] = "required"
|
attrs["required"] = "required"
|
||||||
field.widget.attrs.update(attrs)
|
field.widget.attrs.update(attrs)
|
||||||
|
|
||||||
|
|
||||||
class WarnForm(BootsrapForm):
|
class BaseLogin(BootsrapForm):
|
||||||
"""
|
"""
|
||||||
Bases: :class:`django.forms.Form`
|
Bases: :class:`BootsrapForm`
|
||||||
|
|
||||||
Form used on warn page before emiting a ticket
|
Base form with all field possibly hidden on the login pages
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: The service url for which the user want 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)
|
||||||
|
#: A valid LoginTicket to prevent POST replay
|
||||||
|
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
#: Is the service asking the authentication renewal ?
|
#: 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)
|
#: 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)
|
||||||
|
|
||||||
|
|
||||||
|
class WarnForm(BaseLogin):
|
||||||
|
"""
|
||||||
|
Bases: :class:`BaseLogin`
|
||||||
|
|
||||||
|
Form used on warn page before emiting a ticket
|
||||||
|
"""
|
||||||
#: ``True`` if the user has been warned of the ticket emission
|
#: ``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)
|
|
||||||
|
|
||||||
|
|
||||||
class FederateSelect(BootsrapForm):
|
class FederateSelect(BaseLogin):
|
||||||
"""
|
"""
|
||||||
Bases: :class:`django.forms.Form`
|
Bases: :class:`BaseLogin`
|
||||||
|
|
||||||
Form used on the login page when ``settings.CAS_FEDERATE`` is ``True``
|
Form used on the login page when ``settings.CAS_FEDERATE`` is ``True``
|
||||||
allowing the user to choose an identity provider.
|
allowing the user to choose an identity provider.
|
||||||
@ -76,39 +83,30 @@ class FederateSelect(BootsrapForm):
|
|||||||
to_field_name="suffix",
|
to_field_name="suffix",
|
||||||
label=_('Identity provider'),
|
label=_('Identity provider'),
|
||||||
)
|
)
|
||||||
#: The service url for which the user want a ticket
|
#: A checkbox to ask to be warn before emiting a ticket for another service
|
||||||
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
warn = forms.BooleanField(
|
||||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
label=_('Warn me before logging me into other sites.'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
#: A checkbox to remember the user choices of :attr:`provider<FederateSelect.provider>`
|
#: 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)
|
|
||||||
#: Is the service asking the authentication renewal ?
|
|
||||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
|
||||||
|
|
||||||
|
|
||||||
class UserCredential(BootsrapForm):
|
class UserCredential(BaseLogin):
|
||||||
"""
|
"""
|
||||||
Bases: :class:`django.forms.Form`
|
Bases: :class:`BaseLogin`
|
||||||
|
|
||||||
Form used on the login page to retrive user credentials
|
Form used on the login page to retrive user credentials
|
||||||
"""
|
"""
|
||||||
#: The user username
|
#: The user username
|
||||||
username = forms.CharField(label=_('login'))
|
username = forms.CharField(label=_('username'))
|
||||||
#: The service url for which the user want a ticket
|
|
||||||
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
|
||||||
#: The user password
|
#: 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)
|
|
||||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
||||||
#: A checkbox to ask to be warn before emiting a ticket for another service
|
#: A checkbox to ask to be warn before emiting a ticket for another service
|
||||||
warn = forms.BooleanField(label=_('warn'), required=False)
|
warn = forms.BooleanField(
|
||||||
#: Is the service asking the authentication renewal ?
|
label=_('Warn me before logging me into other sites.'),
|
||||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
required=False
|
||||||
|
)
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(UserCredential, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
@ -124,7 +122,9 @@ class UserCredential(BootsrapForm):
|
|||||||
if auth.test_password(cleaned_data.get("password")):
|
if auth.test_password(cleaned_data.get("password")):
|
||||||
cleaned_data["username"] = auth.username
|
cleaned_data["username"] = auth.username
|
||||||
else:
|
else:
|
||||||
raise forms.ValidationError(_(u"Bad user"))
|
raise forms.ValidationError(
|
||||||
|
_(u"The credentials you provided cannot be determined to be authentic.")
|
||||||
|
)
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
@ -148,21 +148,13 @@ class FederateUserCredential(UserCredential):
|
|||||||
This stub authentication form, allow to implement the federated mode with very few
|
This stub authentication form, allow to implement the federated mode with very few
|
||||||
modificatons to the :class:`LoginView<cas_server.views.LoginView>` view.
|
modificatons to the :class:`LoginView<cas_server.views.LoginView>` view.
|
||||||
"""
|
"""
|
||||||
#: the user username with the ``@`` component
|
|
||||||
username = forms.CharField(widget=forms.HiddenInput())
|
def __init__(self, *args, **kwargs):
|
||||||
#: The service url for which the user want a ticket
|
super(FederateUserCredential, self).__init__(*args, **kwargs)
|
||||||
service = forms.CharField(widget=forms.HiddenInput(), required=False)
|
# All fields are hidden and auto filled by the /login view logic
|
||||||
#: The ``ticket`` used to authenticate the user against a provider
|
for name, field in self.fields.items():
|
||||||
password = forms.CharField(widget=forms.HiddenInput())
|
field.widget = forms.HiddenInput()
|
||||||
#: alias of :attr:`password`
|
self[name].display = False
|
||||||
ticket = forms.CharField(widget=forms.HiddenInput())
|
|
||||||
#: A valid LoginTicket to prevent POST replay
|
|
||||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
||||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
||||||
#: Has the user asked to be warn before emiting a ticket for another service
|
|
||||||
warn = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
|
||||||
#: Is the service asking the authentication renewal ?
|
|
||||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
|
Binary file not shown.
@ -1,371 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the PACKAGE package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: cas_server\n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2016-07-04 17:36+0200\n"
|
|
||||||
"PO-Revision-Date: 2016-07-04 17:39+0200\n"
|
|
||||||
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
|
|
||||||
"Language-Team: django <LL@li.org>\n"
|
|
||||||
"Language: en\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"X-Generator: Poedit 1.8.8\n"
|
|
||||||
|
|
||||||
#: apps.py:19 templates/cas_server/base.html:3
|
|
||||||
#: templates/cas_server/base.html:20
|
|
||||||
msgid "Central Authentication Service"
|
|
||||||
msgstr "Central Authentication Service"
|
|
||||||
|
|
||||||
#: forms.py:43
|
|
||||||
msgid "Identity provider"
|
|
||||||
msgstr "Identity provider"
|
|
||||||
|
|
||||||
#: forms.py:45 forms.py:55 forms.py:106
|
|
||||||
msgid "service"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: forms.py:47
|
|
||||||
msgid "Remember the identity provider"
|
|
||||||
msgstr "Remember the identity provider"
|
|
||||||
|
|
||||||
#: forms.py:48 forms.py:59
|
|
||||||
msgid "warn"
|
|
||||||
msgstr " Warn me before logging me into other sites."
|
|
||||||
|
|
||||||
#: forms.py:54
|
|
||||||
msgid "login"
|
|
||||||
msgstr "username"
|
|
||||||
|
|
||||||
#: forms.py:56
|
|
||||||
msgid "password"
|
|
||||||
msgstr "password"
|
|
||||||
|
|
||||||
#: forms.py:71
|
|
||||||
msgid "Bad user"
|
|
||||||
msgstr "The credentials you provided cannot be determined to be authentic."
|
|
||||||
|
|
||||||
#: forms.py:96
|
|
||||||
msgid "User not found in the temporary database, please try to reconnect"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: management/commands/cas_clean_federate.py:20
|
|
||||||
msgid "Clean old federated users"
|
|
||||||
msgstr "Clean old federated users"
|
|
||||||
|
|
||||||
#: management/commands/cas_clean_sessions.py:22
|
|
||||||
msgid "Clean deleted sessions"
|
|
||||||
msgstr "Clean deleted sessions"
|
|
||||||
|
|
||||||
#: management/commands/cas_clean_tickets.py:22
|
|
||||||
msgid "Clean old trickets"
|
|
||||||
msgstr "Clean old trickets"
|
|
||||||
|
|
||||||
#: models.py:42
|
|
||||||
msgid "identity provider"
|
|
||||||
msgstr "identity provider"
|
|
||||||
|
|
||||||
#: models.py:43
|
|
||||||
msgid "identity providers"
|
|
||||||
msgstr "identity providers"
|
|
||||||
|
|
||||||
#: models.py:47
|
|
||||||
msgid "suffix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:48
|
|
||||||
msgid ""
|
|
||||||
"Suffix append to backend CAS returner username: `returned_username`@`suffix`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:50
|
|
||||||
msgid "server url"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:59
|
|
||||||
msgid "CAS protocol version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:60
|
|
||||||
msgid ""
|
|
||||||
"Version of the CAS protocol to use when sending requests the the backend CAS"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:65
|
|
||||||
msgid "verbose name"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:66
|
|
||||||
msgid "Name for this identity provider displayed on the login page"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:70 models.py:317
|
|
||||||
msgid "position"
|
|
||||||
msgstr "position"
|
|
||||||
|
|
||||||
#: models.py:80
|
|
||||||
msgid "display"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:81
|
|
||||||
msgid "Display the provider on the login page"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:164
|
|
||||||
msgid "User"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:165
|
|
||||||
msgid "Users"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:234
|
|
||||||
#, python-format
|
|
||||||
msgid "Error during service logout %s"
|
|
||||||
msgstr "Error during service logout %s"
|
|
||||||
|
|
||||||
#: models.py:312
|
|
||||||
msgid "Service pattern"
|
|
||||||
msgstr "Service pattern"
|
|
||||||
|
|
||||||
#: models.py:313
|
|
||||||
msgid "Services patterns"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:318
|
|
||||||
msgid "service patterns are sorted using the position attribute"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:325 models.py:449
|
|
||||||
msgid "name"
|
|
||||||
msgstr "name"
|
|
||||||
|
|
||||||
#: models.py:326
|
|
||||||
msgid "A name for the service"
|
|
||||||
msgstr "A name for the service"
|
|
||||||
|
|
||||||
#: models.py:331 models.py:478 models.py:497
|
|
||||||
msgid "pattern"
|
|
||||||
msgstr "pattern"
|
|
||||||
|
|
||||||
#: models.py:333
|
|
||||||
msgid ""
|
|
||||||
"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 '\\'."
|
|
||||||
msgstr ""
|
|
||||||
"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 '\\'."
|
|
||||||
|
|
||||||
#: models.py:342
|
|
||||||
msgid "user field"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:343
|
|
||||||
msgid "Name of the attribut to transmit as username, empty = login"
|
|
||||||
msgstr "Name of the attribut to transmit as username, empty = login"
|
|
||||||
|
|
||||||
#: models.py:347
|
|
||||||
msgid "restrict username"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:348
|
|
||||||
msgid "Limit username allowed to connect to the list provided bellow"
|
|
||||||
msgstr "Limit username allowed to connect to the list provided bellow"
|
|
||||||
|
|
||||||
#: models.py:352
|
|
||||||
msgid "proxy"
|
|
||||||
msgstr "proxy"
|
|
||||||
|
|
||||||
#: models.py:353
|
|
||||||
msgid "Proxy tickets can be delivered to the service"
|
|
||||||
msgstr "Proxy tickets can be delivered to the service"
|
|
||||||
|
|
||||||
#: models.py:357
|
|
||||||
msgid "proxy callback"
|
|
||||||
msgstr "proxy callback"
|
|
||||||
|
|
||||||
#: models.py:358
|
|
||||||
msgid "can be used as a proxy callback to deliver PGT"
|
|
||||||
msgstr "can be used as a proxy callback to deliver PGT"
|
|
||||||
|
|
||||||
#: models.py:362
|
|
||||||
msgid "single log out"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:363
|
|
||||||
msgid "Enable SLO for the service"
|
|
||||||
msgstr "Enable SLO for the service"
|
|
||||||
|
|
||||||
#: models.py:370
|
|
||||||
msgid "single log out callback"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:371
|
|
||||||
msgid ""
|
|
||||||
"URL where the SLO request will be POST. empty = service url\n"
|
|
||||||
"This is usefull for non HTTP proxied services."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:433
|
|
||||||
msgid "username"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: models.py:434
|
|
||||||
msgid "username allowed to connect to the service"
|
|
||||||
msgstr "username allowed to connect to the service"
|
|
||||||
|
|
||||||
#: models.py:450
|
|
||||||
msgid "name of an attribut to send to the service, use * for all attributes"
|
|
||||||
msgstr "name of an attribut to send to the service, use * for all attributes"
|
|
||||||
|
|
||||||
#: models.py:455 models.py:503
|
|
||||||
msgid "replace"
|
|
||||||
msgstr "replace"
|
|
||||||
|
|
||||||
#: models.py:456
|
|
||||||
msgid ""
|
|
||||||
"name under which the attribut will be showto the service. empty = default "
|
|
||||||
"name of the attribut"
|
|
||||||
msgstr ""
|
|
||||||
"name under which the attribut will be showto the service. empty = default "
|
|
||||||
"name of the attribut"
|
|
||||||
|
|
||||||
#: models.py:473 models.py:492
|
|
||||||
msgid "attribut"
|
|
||||||
msgstr "attribut"
|
|
||||||
|
|
||||||
#: models.py:474
|
|
||||||
msgid "Name of the attribut which must verify pattern"
|
|
||||||
msgstr "Name of the attribut which must verify pattern"
|
|
||||||
|
|
||||||
#: models.py:479
|
|
||||||
msgid "a regular expression"
|
|
||||||
msgstr "a regular expression"
|
|
||||||
|
|
||||||
#: models.py:493
|
|
||||||
msgid "Name of the attribut for which the value must be replace"
|
|
||||||
msgstr "Name of the attribut for which the value must be replace"
|
|
||||||
|
|
||||||
#: models.py:498
|
|
||||||
msgid "An regular expression maching whats need to be replaced"
|
|
||||||
msgstr "An regular expression maching whats need to be replaced"
|
|
||||||
|
|
||||||
#: models.py:504
|
|
||||||
msgid "replace expression, groups are capture by \\1, \\2 …"
|
|
||||||
msgstr "replace expression, groups are capture by \\1, \\2 …"
|
|
||||||
|
|
||||||
#: templates/cas_server/logged.html:6
|
|
||||||
msgid "Logged"
|
|
||||||
msgstr ""
|
|
||||||
"<h3>Log In Successful</h3>You have successfully logged into the Central "
|
|
||||||
"Authentication Service.<br/>For security reasons, please Log Out and Exit "
|
|
||||||
"your web browser when you are done accessing services that require "
|
|
||||||
"authentication!"
|
|
||||||
|
|
||||||
#: templates/cas_server/logged.html:10
|
|
||||||
msgid "Log me out from all my sessions"
|
|
||||||
msgstr "Log me out from all my sessions"
|
|
||||||
|
|
||||||
#: templates/cas_server/logged.html:13
|
|
||||||
msgid "Logout"
|
|
||||||
msgstr "Logout"
|
|
||||||
|
|
||||||
#: templates/cas_server/login.html:8
|
|
||||||
msgid "Please log in"
|
|
||||||
msgstr "Please log in"
|
|
||||||
|
|
||||||
#: templates/cas_server/login.html:13
|
|
||||||
msgid "Login"
|
|
||||||
msgstr "Login"
|
|
||||||
|
|
||||||
#: templates/cas_server/warn.html:10
|
|
||||||
msgid "Connect to the service"
|
|
||||||
msgstr "Connect to the service"
|
|
||||||
|
|
||||||
#: views.py:152
|
|
||||||
msgid ""
|
|
||||||
"<h3>Logout successful</h3>You have successfully logged out from the Central "
|
|
||||||
"Authentication Service. For security reasons, exit your web browser."
|
|
||||||
msgstr ""
|
|
||||||
"<h3>Logout successful</h3>You have successfully logged out from the Central "
|
|
||||||
"Authentication Service. For security reasons, exit your web browser."
|
|
||||||
|
|
||||||
#: views.py:158
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"<h3>Logout successful</h3>You have successfully logged out from %s sessions "
|
|
||||||
"of the Central Authentication Service. For security reasons, exit your web "
|
|
||||||
"browser."
|
|
||||||
msgstr ""
|
|
||||||
"<h3>Logout successful</h3>You have successfully logged out from %s sessions "
|
|
||||||
"of the Central Authentication Service. For security reasons, exit your web "
|
|
||||||
"browser."
|
|
||||||
|
|
||||||
#: views.py:165
|
|
||||||
msgid ""
|
|
||||||
"<h3>Logout successful</h3>You were already logged out from the Central "
|
|
||||||
"Authentication Service. For security reasons, exit your web browser."
|
|
||||||
msgstr ""
|
|
||||||
"<h3>Logout successful</h3>You were already logged out from the Central "
|
|
||||||
"Authentication Service. For security reasons, exit your web browser."
|
|
||||||
|
|
||||||
#: views.py:349
|
|
||||||
msgid "Invalid login ticket"
|
|
||||||
msgstr "Invalid login ticket, please retry to login"
|
|
||||||
|
|
||||||
#: views.py:470
|
|
||||||
#, python-format
|
|
||||||
msgid "Authentication has been required by service %(name)s (%(url)s)"
|
|
||||||
msgstr "Authentication has been required by service %(name)s (%(url)s)"
|
|
||||||
|
|
||||||
#: views.py:508
|
|
||||||
#, python-format
|
|
||||||
msgid "Service %(url)s non allowed."
|
|
||||||
msgstr "Service %(url)s non allowed."
|
|
||||||
|
|
||||||
#: views.py:515
|
|
||||||
msgid "Username non allowed"
|
|
||||||
msgstr "Username non allowed"
|
|
||||||
|
|
||||||
#: views.py:522
|
|
||||||
msgid "User charateristics non allowed"
|
|
||||||
msgstr "User charateristics non allowed"
|
|
||||||
|
|
||||||
#: views.py:529
|
|
||||||
#, python-format
|
|
||||||
msgid "The attribut %(field)s is needed to use that service"
|
|
||||||
msgstr "The attribut %(field)s is needed to use that service"
|
|
||||||
|
|
||||||
#: views.py:599
|
|
||||||
#, python-format
|
|
||||||
msgid "Authentication renewal required by service %(name)s (%(url)s)."
|
|
||||||
msgstr "Authentication renewal required by service %(name)s (%(url)s)."
|
|
||||||
|
|
||||||
#: views.py:606
|
|
||||||
#, python-format
|
|
||||||
msgid "Authentication required by service %(name)s (%(url)s)."
|
|
||||||
msgstr "Authentication required by service %(name)s (%(url)s)."
|
|
||||||
|
|
||||||
#: views.py:613
|
|
||||||
#, python-format
|
|
||||||
msgid "Service %s non allowed"
|
|
||||||
msgstr "Service %s non allowed"
|
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "Error during service logout %(service)s:\n"
|
|
||||||
#~ "%(error)s"
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Error during service logout %(service)s:\n"
|
|
||||||
#~ "%(error)s"
|
|
||||||
|
|
||||||
#~ msgid "Successfully logout"
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "<h3>Logout successful</h3>You have successfully logged out of the Central "
|
|
||||||
#~ "Authentication Service.</br>For security reasons, exit your web browser."
|
|
Binary file not shown.
@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: cas_server\n"
|
"Project-Id-Version: cas_server\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2016-07-04 17:36+0200\n"
|
"POT-Creation-Date: 2016-08-01 12:01+0200\n"
|
||||||
"PO-Revision-Date: 2016-07-04 17:37+0200\n"
|
"PO-Revision-Date: 2016-08-01 12:01+0200\n"
|
||||||
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
|
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
|
||||||
"Language-Team: django <LL@li.org>\n"
|
"Language-Team: django <LL@li.org>\n"
|
||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
@ -18,45 +18,45 @@ msgstr ""
|
|||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
"X-Generator: Poedit 1.8.8\n"
|
"X-Generator: Poedit 1.8.8\n"
|
||||||
|
|
||||||
#: apps.py:19 templates/cas_server/base.html:3
|
#: apps.py:25 templates/cas_server/base.html:9
|
||||||
#: templates/cas_server/base.html:20
|
#: templates/cas_server/base.html:27
|
||||||
msgid "Central Authentication Service"
|
msgid "Central Authentication Service"
|
||||||
msgstr "Service Central d'Authentification"
|
msgstr "Service Central d'Authentification"
|
||||||
|
|
||||||
#: forms.py:43
|
#: forms.py:88
|
||||||
msgid "Identity provider"
|
msgid "Identity provider"
|
||||||
msgstr "fournisseur d'identité"
|
msgstr "fournisseur d'identité"
|
||||||
|
|
||||||
#: forms.py:45 forms.py:55 forms.py:106
|
#: forms.py:92 forms.py:111
|
||||||
msgid "service"
|
msgid "Warn me before logging me into other sites."
|
||||||
msgstr "service"
|
msgstr "Prévenez-moi avant d'accéder à d'autres services."
|
||||||
|
|
||||||
#: forms.py:47
|
#: forms.py:96
|
||||||
msgid "Remember the identity provider"
|
msgid "Remember the identity provider"
|
||||||
msgstr "Se souvenir du fournisseur d'identité"
|
msgstr "Se souvenir du fournisseur d'identité"
|
||||||
|
|
||||||
#: forms.py:48 forms.py:59
|
#: forms.py:106 models.py:600
|
||||||
msgid "warn"
|
msgid "username"
|
||||||
msgstr "Prévenez-moi avant d'accéder à d'autres services."
|
msgstr "nom d'utilisateur"
|
||||||
|
|
||||||
#: forms.py:54
|
#: forms.py:108
|
||||||
msgid "login"
|
|
||||||
msgstr "Identifiant"
|
|
||||||
|
|
||||||
#: forms.py:56
|
|
||||||
msgid "password"
|
msgid "password"
|
||||||
msgstr "mot de passe"
|
msgstr "mot de passe"
|
||||||
|
|
||||||
#: forms.py:71
|
#: forms.py:130
|
||||||
msgid "Bad user"
|
msgid "The credentials you provided cannot be determined to be authentic."
|
||||||
msgstr "Les informations transmises n'ont pas permis de vous authentifier."
|
msgstr "Les informations transmises n'ont pas permis de vous authentifier."
|
||||||
|
|
||||||
#: forms.py:96
|
#: forms.py:182
|
||||||
msgid "User not found in the temporary database, please try to reconnect"
|
msgid "User not found in the temporary database, please try to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Utilisateur non trouvé dans la base de donnée temporaire, essayez de vous "
|
"Utilisateur non trouvé dans la base de donnée temporaire, essayez de vous "
|
||||||
"reconnecter"
|
"reconnecter"
|
||||||
|
|
||||||
|
#: forms.py:196
|
||||||
|
msgid "service"
|
||||||
|
msgstr "service"
|
||||||
|
|
||||||
#: management/commands/cas_clean_federate.py:20
|
#: management/commands/cas_clean_federate.py:20
|
||||||
msgid "Clean old federated users"
|
msgid "Clean old federated users"
|
||||||
msgstr "Nettoyer les anciens utilisateurs fédéré"
|
msgstr "Nettoyer les anciens utilisateurs fédéré"
|
||||||
@ -69,98 +69,99 @@ msgstr "Nettoyer les sessions supprimées"
|
|||||||
msgid "Clean old trickets"
|
msgid "Clean old trickets"
|
||||||
msgstr "Nettoyer les vieux tickets"
|
msgstr "Nettoyer les vieux tickets"
|
||||||
|
|
||||||
#: models.py:42
|
#: models.py:46
|
||||||
msgid "identity provider"
|
msgid "identity provider"
|
||||||
msgstr "fournisseur d'identité"
|
msgstr "fournisseur d'identité"
|
||||||
|
|
||||||
#: models.py:43
|
#: models.py:47
|
||||||
msgid "identity providers"
|
msgid "identity providers"
|
||||||
msgstr "fournisseurs d'identités"
|
msgstr "fournisseurs d'identités"
|
||||||
|
|
||||||
#: models.py:47
|
#: models.py:53
|
||||||
msgid "suffix"
|
msgid "suffix"
|
||||||
msgstr "suffixe"
|
msgstr "suffixe"
|
||||||
|
|
||||||
#: models.py:48
|
#: models.py:55
|
||||||
msgid ""
|
msgid ""
|
||||||
"Suffix append to backend CAS returner username: `returned_username`@`suffix`"
|
"Suffix append to backend CAS returned username: ``returned_username`` @ "
|
||||||
|
"``suffix``."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Suffixe ajouté au nom d'utilisateur retourné par le CAS du fournisseur "
|
"Suffixe ajouté au nom d'utilisateur retourné par le CAS du fournisseur "
|
||||||
"d'identité : `nom retourné`@`suffixe`"
|
"d'identité : `nom retourné`@`suffixe`."
|
||||||
|
|
||||||
#: models.py:50
|
#: models.py:62
|
||||||
msgid "server url"
|
msgid "server url"
|
||||||
msgstr "url du serveur"
|
msgstr "url du serveur"
|
||||||
|
|
||||||
#: models.py:59
|
#: models.py:72
|
||||||
msgid "CAS protocol version"
|
msgid "CAS protocol version"
|
||||||
msgstr "Version du protocole CAS"
|
msgstr "Version du protocole CAS"
|
||||||
|
|
||||||
#: models.py:60
|
#: models.py:74
|
||||||
msgid ""
|
msgid ""
|
||||||
"Version of the CAS protocol to use when sending requests the the backend CAS"
|
"Version of the CAS protocol to use when sending requests the the backend CAS."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Version du protocole CAS à utiliser lorsque l'on envoie des requête au CAS "
|
"Version du protocole CAS à utiliser lorsque l'on envoie des requête au CAS "
|
||||||
"du fournisseur d'identité"
|
"du fournisseur d'identité."
|
||||||
|
|
||||||
#: models.py:65
|
#: models.py:81
|
||||||
msgid "verbose name"
|
msgid "verbose name"
|
||||||
msgstr "Nom du fournisseur"
|
msgstr "Nom du fournisseur"
|
||||||
|
|
||||||
#: models.py:66
|
#: models.py:82
|
||||||
msgid "Name for this identity provider displayed on the login page"
|
msgid "Name for this identity provider displayed on the login page."
|
||||||
msgstr "Nom affiché pour ce fournisseur d'identité sur la page de connexion"
|
msgstr "Nom affiché pour ce fournisseur d'identité sur la page de connexion."
|
||||||
|
|
||||||
#: models.py:70 models.py:317
|
#: models.py:88 models.py:446
|
||||||
msgid "position"
|
msgid "position"
|
||||||
msgstr "position"
|
msgstr "position"
|
||||||
|
|
||||||
#: models.py:80
|
#: models.py:102
|
||||||
msgid "display"
|
msgid "display"
|
||||||
msgstr "afficher"
|
msgstr "afficher"
|
||||||
|
|
||||||
#: models.py:81
|
#: models.py:103
|
||||||
msgid "Display the provider on the login page"
|
msgid "Display the provider on the login page."
|
||||||
msgstr "Afficher le fournisseur d'identité sur la page de connexion"
|
msgstr "Afficher le fournisseur d'identité sur la page de connexion."
|
||||||
|
|
||||||
#: models.py:164
|
#: models.py:233
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr "Utilisateur"
|
msgstr "Utilisateur"
|
||||||
|
|
||||||
#: models.py:165
|
#: models.py:234
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr "Utilisateurs"
|
msgstr "Utilisateurs"
|
||||||
|
|
||||||
#: models.py:234
|
#: models.py:320
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Error during service logout %s"
|
msgid "Error during service logout %s"
|
||||||
msgstr "Une erreur est survenue durant la déconnexion du service %s"
|
msgstr "Une erreur est survenue durant la déconnexion du service %s"
|
||||||
|
|
||||||
#: models.py:312
|
#: models.py:440
|
||||||
msgid "Service pattern"
|
msgid "Service pattern"
|
||||||
msgstr "Motif de service"
|
msgstr "Motif de service"
|
||||||
|
|
||||||
#: models.py:313
|
#: models.py:441
|
||||||
msgid "Services patterns"
|
msgid "Services patterns"
|
||||||
msgstr "Motifs de services"
|
msgstr "Motifs de services"
|
||||||
|
|
||||||
#: models.py:318
|
#: models.py:447
|
||||||
msgid "service patterns are sorted using the position attribute"
|
msgid "service patterns are sorted using the position attribute"
|
||||||
msgstr "Les motifs de service sont trié selon l'attribut position"
|
msgstr "Les motifs de service sont trié selon l'attribut position"
|
||||||
|
|
||||||
#: models.py:325 models.py:449
|
#: models.py:455 models.py:626
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr "nom"
|
msgstr "nom"
|
||||||
|
|
||||||
#: models.py:326
|
#: models.py:456
|
||||||
msgid "A name for the service"
|
msgid "A name for the service"
|
||||||
msgstr "Un nom pour le service"
|
msgstr "Un nom pour le service"
|
||||||
|
|
||||||
#: models.py:331 models.py:478 models.py:497
|
#: models.py:464 models.py:669 models.py:698
|
||||||
msgid "pattern"
|
msgid "pattern"
|
||||||
msgstr "motif"
|
msgstr "motif"
|
||||||
|
|
||||||
#: models.py:333
|
#: models.py:466
|
||||||
msgid ""
|
msgid ""
|
||||||
"A regular expression matching services. Will usually looks like '^https://"
|
"A regular expression matching services. Will usually looks like '^https://"
|
||||||
"some\\.server\\.com/path/.*$'.As it is a regular expression, special "
|
"some\\.server\\.com/path/.*$'.As it is a regular expression, special "
|
||||||
@ -171,55 +172,55 @@ msgstr ""
|
|||||||
"expression rationnelle, les caractères spéciaux doivent être échappés avec "
|
"expression rationnelle, les caractères spéciaux doivent être échappés avec "
|
||||||
"un '\\'."
|
"un '\\'."
|
||||||
|
|
||||||
#: models.py:342
|
#: models.py:476
|
||||||
msgid "user field"
|
msgid "user field"
|
||||||
msgstr "champ utilisateur"
|
msgstr "champ utilisateur"
|
||||||
|
|
||||||
#: models.py:343
|
#: models.py:477
|
||||||
msgid "Name of the attribut to transmit as username, empty = login"
|
msgid "Name of the attribute to transmit as username, empty = login"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Nom de l'attribut devant être transmis comme nom d'utilisateur au service. "
|
"Nom de l'attribut devant être transmis comme nom d'utilisateur au service. "
|
||||||
"vide = nom de connection"
|
"vide = nom de connexion"
|
||||||
|
|
||||||
#: models.py:347
|
#: models.py:482
|
||||||
msgid "restrict username"
|
msgid "restrict username"
|
||||||
msgstr "limiter les noms d'utilisateurs"
|
msgstr "limiter les noms d'utilisateurs"
|
||||||
|
|
||||||
#: models.py:348
|
#: models.py:483
|
||||||
msgid "Limit username allowed to connect to the list provided bellow"
|
msgid "Limit username allowed to connect to the list provided bellow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie "
|
"Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie "
|
||||||
"ci-dessous"
|
"ci-dessous"
|
||||||
|
|
||||||
#: models.py:352
|
#: models.py:488
|
||||||
msgid "proxy"
|
msgid "proxy"
|
||||||
msgstr "proxy"
|
msgstr "proxy"
|
||||||
|
|
||||||
#: models.py:353
|
#: models.py:489
|
||||||
msgid "Proxy tickets can be delivered to the service"
|
msgid "Proxy tickets can be delivered to the service"
|
||||||
msgstr "des proxy tickets peuvent être délivrés au service"
|
msgstr "des proxy tickets peuvent être délivrés au service"
|
||||||
|
|
||||||
#: models.py:357
|
#: models.py:495
|
||||||
msgid "proxy callback"
|
msgid "proxy callback"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:358
|
#: models.py:496
|
||||||
msgid "can be used as a proxy callback to deliver PGT"
|
msgid "can be used as a proxy callback to deliver PGT"
|
||||||
msgstr "peut être utilisé comme un callback pour recevoir un PGT"
|
msgstr "peut être utilisé comme un callback pour recevoir un PGT"
|
||||||
|
|
||||||
#: models.py:362
|
#: models.py:503
|
||||||
msgid "single log out"
|
msgid "single log out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:363
|
#: models.py:504
|
||||||
msgid "Enable SLO for the service"
|
msgid "Enable SLO for the service"
|
||||||
msgstr "Active le SLO pour le service"
|
msgstr "Active le SLO pour le service"
|
||||||
|
|
||||||
#: models.py:370
|
#: models.py:512
|
||||||
msgid "single log out callback"
|
msgid "single log out callback"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:371
|
#: models.py:513
|
||||||
msgid ""
|
msgid ""
|
||||||
"URL where the SLO request will be POST. empty = service url\n"
|
"URL where the SLO request will be POST. empty = service url\n"
|
||||||
"This is usefull for non HTTP proxied services."
|
"This is usefull for non HTTP proxied services."
|
||||||
@ -228,83 +229,98 @@ msgstr ""
|
|||||||
"service\n"
|
"service\n"
|
||||||
"Ceci n'est utilise que pour des services non HTTP proxifiés"
|
"Ceci n'est utilise que pour des services non HTTP proxifiés"
|
||||||
|
|
||||||
#: models.py:433
|
#: models.py:601
|
||||||
msgid "username"
|
|
||||||
msgstr "nom d'utilisateur"
|
|
||||||
|
|
||||||
#: models.py:434
|
|
||||||
msgid "username allowed to connect to the service"
|
msgid "username allowed to connect to the service"
|
||||||
msgstr "noms d'utilisateurs autorisé à se connecter au service"
|
msgstr "noms d'utilisateurs autorisé à se connecter au service"
|
||||||
|
|
||||||
#: models.py:450
|
#: models.py:627
|
||||||
msgid "name of an attribut to send to the service, use * for all attributes"
|
msgid "name of an attribute to send to the service, use * for all attributes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"nom d'un attribut a envoyer au service, utiliser * pour tous les attributs"
|
"nom d'un attribut a envoyer au service, utiliser * pour tous les attributs"
|
||||||
|
|
||||||
#: models.py:455 models.py:503
|
#: models.py:634 models.py:705
|
||||||
msgid "replace"
|
msgid "replace"
|
||||||
msgstr "remplacement"
|
msgstr "remplacement"
|
||||||
|
|
||||||
#: models.py:456
|
#: models.py:635
|
||||||
msgid ""
|
msgid ""
|
||||||
"name under which the attribut will be showto the service. empty = default "
|
"name under which the attribute will be showto the service. empty = default "
|
||||||
"name of the attribut"
|
"name of the attribut"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"nom sous lequel l'attribut sera rendu visible au service. vide = inchangé"
|
"nom sous lequel l'attribut sera rendu visible au service. vide = inchangé"
|
||||||
|
|
||||||
#: models.py:473 models.py:492
|
#: models.py:662 models.py:692
|
||||||
msgid "attribut"
|
msgid "attribute"
|
||||||
msgstr "attribut"
|
msgstr "attribut"
|
||||||
|
|
||||||
#: models.py:474
|
#: models.py:663
|
||||||
msgid "Name of the attribut which must verify pattern"
|
msgid "Name of the attribute which must verify pattern"
|
||||||
msgstr "Nom de l'attribut devant vérifier un motif"
|
msgstr "Nom de l'attribut devant vérifier un motif"
|
||||||
|
|
||||||
#: models.py:479
|
#: models.py:670
|
||||||
msgid "a regular expression"
|
msgid "a regular expression"
|
||||||
msgstr "une expression régulière"
|
msgstr "une expression régulière"
|
||||||
|
|
||||||
#: models.py:493
|
#: models.py:693
|
||||||
msgid "Name of the attribut for which the value must be replace"
|
msgid "Name of the attribute for which the value must be replace"
|
||||||
msgstr "nom de l'attribue pour lequel la valeur doit être remplacé"
|
msgstr "nom de l'attribut pour lequel la valeur doit être remplacé"
|
||||||
|
|
||||||
#: models.py:498
|
#: models.py:699
|
||||||
msgid "An regular expression maching whats need to be replaced"
|
msgid "An regular expression maching whats need to be replaced"
|
||||||
msgstr "une expression régulière reconnaissant ce qui doit être remplacé"
|
msgstr "une expression régulière reconnaissant ce qui doit être remplacé"
|
||||||
|
|
||||||
#: models.py:504
|
#: models.py:706
|
||||||
msgid "replace expression, groups are capture by \\1, \\2 …"
|
msgid "replace expression, groups are capture by \\1, \\2 …"
|
||||||
msgstr "expression de remplacement, les groupe sont capturé par \\1, \\2"
|
msgstr "expression de remplacement, les groupe sont capturé par \\1, \\2"
|
||||||
|
|
||||||
#: templates/cas_server/logged.html:6
|
#: templates/cas_server/base.html:38
|
||||||
msgid "Logged"
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"A new version of the application is available. This instance runs "
|
||||||
|
"%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider "
|
||||||
|
"upgrading."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"<h3>Connexion réussie</h3>Vous vous êtes authentifié(e) auprès du Service "
|
"Une nouvelle version de l'application est disponible. Cette instance utilise "
|
||||||
"Central d'Authentification.<br/>Pour des raisons de sécurité, veuillez vous "
|
"la version %(VERSION)s et la dernière version est %(LAST_VERSION)s. Merci de "
|
||||||
"déconnecter et fermer votre navigateur lorsque vous avez fini d'accéder aux "
|
"vous mettre a jour."
|
||||||
"services authentifiés."
|
|
||||||
|
|
||||||
#: templates/cas_server/logged.html:10
|
#: templates/cas_server/logged.html:4
|
||||||
|
msgid ""
|
||||||
|
"<h3>Log In Successful</h3>You have successfully logged into the Central "
|
||||||
|
"Authentication Service.<br/>For security reasons, please Log Out and Exit "
|
||||||
|
"your web browser when you are done accessing services that require "
|
||||||
|
"authentication!"
|
||||||
|
msgstr ""
|
||||||
|
"<h3>Déconnexion réussie</h3>Vous vous êtes déconnecté(e) du Service Central "
|
||||||
|
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
|
||||||
|
"navigateur après avoir fini d'accéder a des services demandant une "
|
||||||
|
"authentification !"
|
||||||
|
|
||||||
|
#: templates/cas_server/logged.html:8
|
||||||
msgid "Log me out from all my sessions"
|
msgid "Log me out from all my sessions"
|
||||||
msgstr "Me déconnecter de toutes mes sessions"
|
msgstr "Me déconnecter de toutes mes sessions"
|
||||||
|
|
||||||
#: templates/cas_server/logged.html:13
|
#: templates/cas_server/logged.html:14
|
||||||
|
msgid "Forget the identity provider"
|
||||||
|
msgstr "Oublier le fournisseur d'identité"
|
||||||
|
|
||||||
|
#: templates/cas_server/logged.html:18
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Se déconnecter"
|
msgstr "Se déconnecter"
|
||||||
|
|
||||||
#: templates/cas_server/login.html:8
|
#: templates/cas_server/login.html:6
|
||||||
msgid "Please log in"
|
msgid "Please log in"
|
||||||
msgstr "Merci de se connecter"
|
msgstr "Merci de se connecter"
|
||||||
|
|
||||||
#: templates/cas_server/login.html:13
|
#: templates/cas_server/login.html:14
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Connexion"
|
msgstr "Connexion"
|
||||||
|
|
||||||
#: templates/cas_server/warn.html:10
|
#: templates/cas_server/warn.html:9
|
||||||
msgid "Connect to the service"
|
msgid "Connect to the service"
|
||||||
msgstr "Se connecter au service"
|
msgstr "Se connecter au service"
|
||||||
|
|
||||||
#: views.py:152
|
#: views.py:168
|
||||||
msgid ""
|
msgid ""
|
||||||
"<h3>Logout successful</h3>You have successfully logged out from the Central "
|
"<h3>Logout successful</h3>You have successfully logged out from the Central "
|
||||||
"Authentication Service. For security reasons, exit your web browser."
|
"Authentication Service. For security reasons, exit your web browser."
|
||||||
@ -313,7 +329,7 @@ msgstr ""
|
|||||||
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
|
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
|
||||||
"navigateur."
|
"navigateur."
|
||||||
|
|
||||||
#: views.py:158
|
#: views.py:174
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"<h3>Logout successful</h3>You have successfully logged out from %s sessions "
|
"<h3>Logout successful</h3>You have successfully logged out from %s sessions "
|
||||||
@ -324,7 +340,7 @@ msgstr ""
|
|||||||
"Service Central d'Authentification. Pour des raisons de sécurité, veuillez "
|
"Service Central d'Authentification. Pour des raisons de sécurité, veuillez "
|
||||||
"fermer votre navigateur."
|
"fermer votre navigateur."
|
||||||
|
|
||||||
#: views.py:165
|
#: views.py:181
|
||||||
msgid ""
|
msgid ""
|
||||||
"<h3>Logout successful</h3>You were already logged out from the Central "
|
"<h3>Logout successful</h3>You were already logged out from the Central "
|
||||||
"Authentication Service. For security reasons, exit your web browser."
|
"Authentication Service. For security reasons, exit your web browser."
|
||||||
@ -333,50 +349,75 @@ msgstr ""
|
|||||||
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
|
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
|
||||||
"navigateur."
|
"navigateur."
|
||||||
|
|
||||||
#: views.py:349
|
#: views.py:361
|
||||||
msgid "Invalid login ticket"
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Invalid response from your identity provider CAS upon ticket %(ticket)s "
|
||||||
|
"validation: %(error)r"
|
||||||
|
msgstr ""
|
||||||
|
"Réponse invalide du CAS du fournisseur d'identité lors de la validation du "
|
||||||
|
"ticket %(ticket)s: %(error)r"
|
||||||
|
|
||||||
|
#: views.py:483
|
||||||
|
msgid "Invalid login ticket, please retry to login"
|
||||||
msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter"
|
msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter"
|
||||||
|
|
||||||
#: views.py:470
|
#: views.py:675
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Authentication has been required by service %(name)s (%(url)s)"
|
msgid "Authentication has been required by service %(name)s (%(url)s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Une demande d'authentification a été émise pour le service %(name)s "
|
"Une demande d'authentification a été émise pour le service %(name)s "
|
||||||
"(%(url)s)."
|
"(%(url)s)."
|
||||||
|
|
||||||
#: views.py:508
|
#: views.py:713
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Service %(url)s non allowed."
|
msgid "Service %(url)s non allowed."
|
||||||
msgstr "le service %(url)s n'est pas autorisé."
|
msgstr "le service %(url)s n'est pas autorisé."
|
||||||
|
|
||||||
#: views.py:515
|
#: views.py:720
|
||||||
msgid "Username non allowed"
|
msgid "Username non allowed"
|
||||||
msgstr "Nom d'utilisateur non authorisé"
|
msgstr "Nom d'utilisateur non authorisé"
|
||||||
|
|
||||||
#: views.py:522
|
#: views.py:727
|
||||||
msgid "User charateristics non allowed"
|
msgid "User characteristics non allowed"
|
||||||
msgstr "Caractéristique utilisateur non autorisée"
|
msgstr "Caractéristique utilisateur non autorisée"
|
||||||
|
|
||||||
#: views.py:529
|
#: views.py:734
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The attribut %(field)s is needed to use that service"
|
msgid "The attribute %(field)s is needed to use that service"
|
||||||
msgstr "L'attribut %(field)s est nécessaire pour se connecter à ce service"
|
msgstr "L'attribut %(field)s est nécessaire pour se connecter à ce service"
|
||||||
|
|
||||||
#: views.py:599
|
#: views.py:824
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Authentication renewal required by service %(name)s (%(url)s)."
|
msgid "Authentication renewal required by service %(name)s (%(url)s)."
|
||||||
msgstr "Demande de réauthentification pour le service %(name)s (%(url)s)."
|
msgstr "Demande de réauthentification pour le service %(name)s (%(url)s)."
|
||||||
|
|
||||||
#: views.py:606
|
#: views.py:831
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Authentication required by service %(name)s (%(url)s)."
|
msgid "Authentication required by service %(name)s (%(url)s)."
|
||||||
msgstr "Authentification requise par le service %(name)s (%(url)s)."
|
msgstr "Authentification requise par le service %(name)s (%(url)s)."
|
||||||
|
|
||||||
#: views.py:613
|
#: views.py:838
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Service %s non allowed"
|
msgid "Service %s non allowed"
|
||||||
msgstr "Le service %s n'est pas autorisé"
|
msgstr "Le service %s n'est pas autorisé"
|
||||||
|
|
||||||
|
#~ msgid "Logged"
|
||||||
|
#~ msgstr ""
|
||||||
|
#~ "<h3>Connexion réussie</h3>Vous vous êtes authentifié(e) auprès du Service "
|
||||||
|
#~ "Central d'Authentification.<br/>Pour des raisons de sécurité, veuillez "
|
||||||
|
#~ "vous déconnecter et fermer votre navigateur lorsque vous avez fini "
|
||||||
|
#~ "d'accéder aux services authentifiés."
|
||||||
|
|
||||||
|
#~ msgid "warn"
|
||||||
|
#~ msgstr "Prévenez-moi avant d'accéder à d'autres services."
|
||||||
|
|
||||||
|
#~ msgid "login"
|
||||||
|
#~ msgstr "Identifiant"
|
||||||
|
|
||||||
|
#~ msgid "Bad user"
|
||||||
|
#~ msgstr "Les informations transmises n'ont pas permis de vous authentifier."
|
||||||
|
|
||||||
#~ msgid ""
|
#~ msgid ""
|
||||||
#~ "Error during service logout %(service)s:\n"
|
#~ "Error during service logout %(service)s:\n"
|
||||||
#~ "%(error)s"
|
#~ "%(error)s"
|
||||||
|
@ -23,3 +23,4 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
models.User.clean_deleted_sessions()
|
models.User.clean_deleted_sessions()
|
||||||
|
models.NewVersionWarning.send_mails()
|
||||||
|
22
cas_server/migrations/0008_newversionwarning.py
Normal file
22
cas_server/migrations/0008_newversionwarning.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-07-27 21:59
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cas_server', '0007_auto_20160723_2252'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='NewVersionWarning',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('version', models.CharField(max_length=255)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -18,15 +18,18 @@ 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 django.core.mail import send_mail
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import smtplib
|
||||||
import logging
|
import logging
|
||||||
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
|
||||||
|
from . import VERSION
|
||||||
|
|
||||||
#: logger facility
|
#: logger facility
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -465,13 +468,13 @@ 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
|
#: Name of the attribute 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="",
|
||||||
blank=True,
|
blank=True,
|
||||||
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 attribute to transmit as username, empty = login")
|
||||||
)
|
)
|
||||||
#: A boolean allowing to limit username allowed to connect to :attr:`usernames`.
|
#: A boolean allowing to limit username allowed to connect to :attr:`usernames`.
|
||||||
restrict_users = models.BooleanField(
|
restrict_users = models.BooleanField(
|
||||||
@ -621,7 +624,7 @@ class ReplaceAttributName(models.Model):
|
|||||||
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 attribute 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`
|
#: The name of the attribute to transmit to the service. If empty, the value of :attr:`name`
|
||||||
#: is used.
|
#: is used.
|
||||||
@ -629,7 +632,7 @@ class ReplaceAttributName(models.Model):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_(u"replace"),
|
verbose_name=_(u"replace"),
|
||||||
help_text=_(u"name under which the attribut will be show"
|
help_text=_(u"name under which the attribute 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
|
#: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributName` instances for a
|
||||||
@ -656,8 +659,8 @@ class FilterAttributValue(models.Model):
|
|||||||
#: The name of a user attribute
|
#: 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"attribute"),
|
||||||
help_text=_(u"Name of the attribut which must verify pattern")
|
help_text=_(u"Name of the attribute which must verify pattern")
|
||||||
)
|
)
|
||||||
#: A regular expression the attribute :attr:`attribut` value must verify. If :attr:`attribut`
|
#: 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.
|
#: if a list, only one of the list values needs to match.
|
||||||
@ -686,8 +689,8 @@ class ReplaceAttributValue(models.Model):
|
|||||||
#: Name the attribute: a key of :attr:`User.attributs`
|
#: 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"attribute"),
|
||||||
help_text=_(u"Name of the attribut for which the value must be replace")
|
help_text=_(u"Name of the attribute for which the value must be replace")
|
||||||
)
|
)
|
||||||
#: A regular expression matching the part of the attribute value that need to be changed
|
#: A regular expression matching the part of the attribute value that need to be changed
|
||||||
pattern = models.CharField(
|
pattern = models.CharField(
|
||||||
@ -1003,3 +1006,60 @@ class Proxy(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.url
|
return self.url
|
||||||
|
|
||||||
|
|
||||||
|
class NewVersionWarning(models.Model):
|
||||||
|
"""
|
||||||
|
Bases: :class:`django.db.models.Model`
|
||||||
|
|
||||||
|
The last new version available version sent
|
||||||
|
"""
|
||||||
|
version = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def send_mails(cls):
|
||||||
|
"""
|
||||||
|
For each new django-cas-server version, if the current instance is not up to date
|
||||||
|
send one mail to ``settings.ADMINS``.
|
||||||
|
"""
|
||||||
|
if settings.CAS_NEW_VERSION_EMAIL_WARNING and settings.ADMINS:
|
||||||
|
try:
|
||||||
|
obj = cls.objects.get()
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
obj = NewVersionWarning.objects.create(version=VERSION)
|
||||||
|
LAST_VERSION = utils.last_version()
|
||||||
|
if LAST_VERSION is not None and LAST_VERSION != obj.version:
|
||||||
|
if utils.decode_version(VERSION) < utils.decode_version(LAST_VERSION):
|
||||||
|
try:
|
||||||
|
send_mail(
|
||||||
|
(
|
||||||
|
'%sA new version of django-cas-server is available'
|
||||||
|
) % settings.EMAIL_SUBJECT_PREFIX,
|
||||||
|
u'''
|
||||||
|
A new version of the django-cas-server is available.
|
||||||
|
|
||||||
|
Your version: %s
|
||||||
|
New version: %s
|
||||||
|
|
||||||
|
Upgrade using:
|
||||||
|
* pip install -U django-cas-server
|
||||||
|
* fetching the last release on
|
||||||
|
https://github.com/nitmir/django-cas-server/ or on
|
||||||
|
https://pypi.python.org/pypi/django-cas-server
|
||||||
|
|
||||||
|
After upgrade, do not forget to run:
|
||||||
|
* ./manage.py migrate
|
||||||
|
* ./manage.py collectstatic
|
||||||
|
and to reload your wsgi server (apache2, uwsgi, gunicord, etc…)
|
||||||
|
|
||||||
|
--\u0020
|
||||||
|
django-cas-server
|
||||||
|
'''.strip() % (VERSION, LAST_VERSION),
|
||||||
|
settings.SERVER_EMAIL,
|
||||||
|
["%s <%s>" % admin for admin in settings.ADMINS],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
obj.version = LAST_VERSION
|
||||||
|
obj.save()
|
||||||
|
except smtplib.SMTPException as error: # pragma: no cover (should not happen)
|
||||||
|
logger.error("Unable to send new version mail: %s" % error)
|
||||||
|
27
cas_server/static/cas_server/alert-version.js
Normal file
27
cas_server/static/cas_server/alert-version.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
function alert_version(last_version){
|
||||||
|
jQuery(function( $ ){
|
||||||
|
$("#alert-version").click(function( e ){
|
||||||
|
e.preventDefault();
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(date.getTime()+(10*365*24*60*60*1000));
|
||||||
|
var expires = "; expires="+date.toGMTString();
|
||||||
|
document.cookie = "cas-alert-version=" + last_version + expires + "; path=/";
|
||||||
|
});
|
||||||
|
|
||||||
|
var nameEQ="cas-alert-version=";
|
||||||
|
var ca = document.cookie.split(";");
|
||||||
|
var value;
|
||||||
|
for(var i=0;i < ca.length;i++) {
|
||||||
|
var c = ca[i];
|
||||||
|
while(c.charAt(0) === " "){
|
||||||
|
c = c.substring(1,c.length);
|
||||||
|
}
|
||||||
|
if(c.indexOf(nameEQ) === 0){
|
||||||
|
value = c.substring(nameEQ.length,c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(value === last_version){
|
||||||
|
$("#alert-version").parent().hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -14,8 +14,8 @@
|
|||||||
<script src="{{settings.CAS_COMPONENT_URLS.html5shiv}}"></script>
|
<script src="{{settings.CAS_COMPONENT_URLS.html5shiv}}"></script>
|
||||||
<script src="{{settings.CAS_COMPONENT_URLS.respond}}"></script>
|
<script src="{{settings.CAS_COMPONENT_URLS.respond}}"></script>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<link rel="shortcut icon" href="{% static "cas_server/favicon.ico?v=1" %}" />
|
{% if settings.CAS_FAVICON_URL %}<link rel="shortcut icon" href="{{settings.CAS_FAVICON_URL}}" />{% endif %}
|
||||||
<link href="{% static "cas_server/login.css" %}" rel="stylesheet">
|
<link href="{% static "cas_server/styles.css" %}" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -31,23 +31,28 @@
|
|||||||
<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">
|
||||||
{% block ante_messages %}{% endblock %}
|
|
||||||
{% if auto_submit %}<noscript>{% endif %}
|
{% if auto_submit %}<noscript>{% endif %}
|
||||||
|
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
|
||||||
|
<div class="alert alert-info alert-dismissable">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="alert-version">×</button>
|
||||||
|
{% blocktrans %}A new version of the application is available. This instance runs {{VERSION}} and the last version is {{LAST_VERSION}}. Please consider upgrading.{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% block ante_messages %}{% endblock %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div {% spaceless %}
|
<div {% spaceless %}
|
||||||
{% if message.level == message_levels.DEBUG %}
|
{% if message.level == message_levels.DEBUG %}
|
||||||
class="alert alert-warning alert-dismissable"
|
class="alert alert-warning"
|
||||||
{% elif message.level == message_levels.INFO %}
|
{% elif message.level == message_levels.INFO %}
|
||||||
class="alert alert-info alert-dismissable"
|
class="alert alert-info"
|
||||||
{% elif message.level == message_levels.SUCCESS %}
|
{% elif message.level == message_levels.SUCCESS %}
|
||||||
class="alert alert-success alert-dismissable"
|
class="alert alert-success"
|
||||||
{% elif message.level == message_levels.WARNING %}
|
{% elif message.level == message_levels.WARNING %}
|
||||||
class="alert alert-warning alert-dismissable"
|
class="alert alert-warning"
|
||||||
{% else %}
|
{% else %}
|
||||||
class="alert alert-danger alert-dismissable"
|
class="alert alert-danger"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endspaceless %}>
|
{% endspaceless %}>
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -59,5 +64,9 @@
|
|||||||
</div> <!-- /container -->
|
</div> <!-- /container -->
|
||||||
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
|
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
|
||||||
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
|
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
|
||||||
|
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
|
||||||
|
<script src="{% static "cas_server/alert-version.js" %}"></script>
|
||||||
|
<script>alert_version("{{LAST_VERSION}}")</script>
|
||||||
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
{% load cas_server %}
|
||||||
{% for error in form.non_field_errors %}
|
{% for error in form.non_field_errors %}
|
||||||
<div class="alert alert-danger alert-dismissable">
|
<div class="alert alert-danger alert-dismissable">
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||||
{{error}}
|
{{error}}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for field in form %}{% if field.display %}
|
{% for field in form %}{% if not field|is_hidden %}
|
||||||
<div class="form-group{% spaceless %}
|
<div class="form-group{% spaceless %}
|
||||||
{% if not form.non_field_errors %}
|
{% if not form.non_field_errors %}
|
||||||
{% if field.errors %} has-error
|
{% if field.errors %} has-error
|
||||||
@ -12,7 +13,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}"
|
{% endif %}"
|
||||||
{% endspaceless %}>{% spaceless %}
|
{% endspaceless %}>{% spaceless %}
|
||||||
{% if field.checkbox %}
|
{% if field|is_checkbox %}
|
||||||
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label>
|
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label>
|
||||||
{% else %}
|
{% else %}
|
||||||
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
|
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
{% extends "cas_server/base.html" %}
|
{% extends "cas_server/base.html" %}
|
||||||
{% 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">{% blocktrans %}<h3>Log In Successful</h3>You have successfully logged into the Central Authentication Service.<br/>For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!{% endblocktrans %}</div>
|
||||||
<form class="form-signin" method="get" action="logout">
|
<form class="form-signin" method="get" action="logout">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<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>
|
||||||
|
{% if settings.CAS_FEDERATE and request.COOKIES.remember_provider %}
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="forget_provider" value="1">{% trans "Forget the identity provider" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button>
|
<button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
0
cas_server/templatetags/__init__.py
Normal file
0
cas_server/templatetags/__init__.py
Normal file
14
cas_server/templatetags/cas_server.py
Normal file
14
cas_server/templatetags/cas_server.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django import template
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='is_checkbox')
|
||||||
|
def is_checkbox(field):
|
||||||
|
return isinstance(field.field.widget, forms.CheckboxInput)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='is_hidden')
|
||||||
|
def is_hidden(field):
|
||||||
|
return isinstance(field.field.widget, forms.HiddenInput)
|
@ -51,6 +51,22 @@ MIDDLEWARE_CLASSES = [
|
|||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'cas_server.tests.urls'
|
ROOT_URLCONF = 'cas_server.tests.urls'
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
@ -81,3 +97,30 @@ USE_TZ = True
|
|||||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
CAS_NEW_VERSION_HTML_WARNING = False
|
||||||
|
CAS_NEW_VERSION_EMAIL_WARNING = False
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'cas_file': {
|
||||||
|
'format': '%(asctime)s %(levelname)s %(message)s'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'cas_stream': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'cas_file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'cas_server': {
|
||||||
|
'handlers': ['cas_stream'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -84,29 +84,31 @@ class FederateAuthLoginLogoutTestCase(
|
|||||||
params['provider'] = provider.suffix
|
params['provider'] = provider.suffix
|
||||||
if remember:
|
if remember:
|
||||||
params['remember'] = 'on'
|
params['remember'] = 'on'
|
||||||
|
# just try for one suffix
|
||||||
|
if suffix == "example.com":
|
||||||
|
# if renew=False is posted it should be ignored
|
||||||
|
params["renew"] = False
|
||||||
# post the choosed provider
|
# post the choosed provider
|
||||||
response = client.post('/federate', params)
|
response = client.post('/federate', params)
|
||||||
# we are redirected to the provider CAS client url
|
# we are redirected to the provider CAS client url
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
if remember:
|
self.assertEqual(response["Location"], '%s/federate/%s%s' % (
|
||||||
self.assertEqual(response["Location"], '%s/federate/%s?remember=on' % (
|
|
||||||
'http://testserver' if django.VERSION < (1, 9) else "",
|
'http://testserver' if django.VERSION < (1, 9) else "",
|
||||||
provider.suffix
|
provider.suffix,
|
||||||
))
|
"?remember=on" if remember else ""
|
||||||
else:
|
|
||||||
self.assertEqual(response["Location"], '%s/federate/%s' % (
|
|
||||||
'http://testserver' if django.VERSION < (1, 9) else "",
|
|
||||||
provider.suffix
|
|
||||||
))
|
))
|
||||||
# let's follow the redirect
|
# let's follow the redirect
|
||||||
response = client.get('/federate/%s' % provider.suffix)
|
response = client.get(
|
||||||
|
'/federate/%s%s' % (provider.suffix, "?remember=on" if remember else "")
|
||||||
|
)
|
||||||
# we are redirected to the provider CAS for authentication
|
# we are redirected to the provider CAS for authentication
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response["Location"],
|
response["Location"],
|
||||||
"%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % (
|
"%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s%s" % (
|
||||||
provider.server_url,
|
provider.server_url,
|
||||||
provider.suffix
|
provider.suffix,
|
||||||
|
"%3Fremember%3Don" if remember else ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# let's generate a ticket
|
# let's generate a ticket
|
||||||
@ -114,7 +116,10 @@ class FederateAuthLoginLogoutTestCase(
|
|||||||
# we lauch a dummy CAS server that only validate once for the service
|
# we lauch a dummy CAS server that only validate once for the service
|
||||||
# http://testserver/federate/example.com with `ticket`
|
# http://testserver/federate/example.com with `ticket`
|
||||||
tests_utils.DummyCAS.run(
|
tests_utils.DummyCAS.run(
|
||||||
("http://testserver/federate/%s" % provider.suffix).encode("ascii"),
|
("http://testserver/federate/%s%s" % (
|
||||||
|
provider.suffix,
|
||||||
|
"?remember=on" if remember else ""
|
||||||
|
)).encode("ascii"),
|
||||||
ticket.encode("ascii"),
|
ticket.encode("ascii"),
|
||||||
settings.CAS_TEST_USER.encode("utf8"),
|
settings.CAS_TEST_USER.encode("utf8"),
|
||||||
[],
|
[],
|
||||||
@ -122,7 +127,13 @@ class FederateAuthLoginLogoutTestCase(
|
|||||||
)
|
)
|
||||||
# we normally provide a good ticket and should be redirected to /login as the ticket
|
# we normally provide a good ticket and should be redirected to /login as the ticket
|
||||||
# get successfully validated again the dummy CAS
|
# get successfully validated again the dummy CAS
|
||||||
response = client.get('/federate/%s' % provider.suffix, {'ticket': ticket})
|
response = client.get(
|
||||||
|
'/federate/%s' % provider.suffix,
|
||||||
|
{'ticket': ticket, 'remember': 'on' if remember else ''}
|
||||||
|
)
|
||||||
|
if remember:
|
||||||
|
self.assertIn("remember_provider", client.cookies)
|
||||||
|
self.assertEqual(client.cookies["remember_provider"].value, provider.suffix)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response["Location"], "%s/login" % (
|
self.assertEqual(response["Location"], "%s/login" % (
|
||||||
'http://testserver' if django.VERSION < (1, 9) else ""
|
'http://testserver' if django.VERSION < (1, 9) else ""
|
||||||
@ -183,7 +194,8 @@ class FederateAuthLoginLogoutTestCase(
|
|||||||
"""
|
"""
|
||||||
The federated view should redirect to /login if the provider is unknown or not provided,
|
The federated view should redirect to /login if the provider is unknown or not provided,
|
||||||
try to fetch a new ticket if the provided ticket validation fail
|
try to fetch a new ticket if the provided ticket validation fail
|
||||||
(network error or bad ticket)
|
(network error or bad ticket), redirect to /login with a error message if identity
|
||||||
|
provider CAS return a bad response (invalid XML document)
|
||||||
"""
|
"""
|
||||||
good_provider = "example.com"
|
good_provider = "example.com"
|
||||||
bad_provider = "exemple.fr"
|
bad_provider = "exemple.fr"
|
||||||
@ -229,6 +241,18 @@ class FederateAuthLoginLogoutTestCase(
|
|||||||
'http://testserver' if django.VERSION < (1, 9) else ""
|
'http://testserver' if django.VERSION < (1, 9) else ""
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# test CAS avaible but return a bad XML doc, should redirect to /login with a error message
|
||||||
|
# use "example.net" as it is CASv3
|
||||||
|
tests_utils.HttpParamsHandler.run(8082)
|
||||||
|
response = client.get("/federate/%s" % "example.net", {'ticket': utils.gen_st()})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response["Location"], "%s/login" % (
|
||||||
|
'http://testserver' if django.VERSION < (1, 9) else ""
|
||||||
|
))
|
||||||
|
response = client.get("/login")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b"Invalid response from your identity provider CAS", response.content)
|
||||||
|
|
||||||
def test_auth_federate_slo(self):
|
def test_auth_federate_slo(self):
|
||||||
"""test that SLO receive from backend CAS log out the users"""
|
"""test that SLO receive from backend CAS log out the users"""
|
||||||
# get tickets and connected clients
|
# get tickets and connected clients
|
||||||
@ -331,6 +355,76 @@ class FederateAuthLoginLogoutTestCase(
|
|||||||
provider.suffix
|
provider.suffix
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def test_forget_provider(self):
|
||||||
|
"""Test the logout option to forget remembered provider"""
|
||||||
|
tickets = self.test_login_post_provider(remember=True)
|
||||||
|
for (provider, _, client) in tickets:
|
||||||
|
self.assertIn("remember_provider", client.cookies)
|
||||||
|
self.assertEqual(client.cookies["remember_provider"].value, provider.suffix)
|
||||||
|
self.assertNotEqual(client.cookies["remember_provider"]["max-age"], 0)
|
||||||
|
client.get("/logout?forget_provider=1")
|
||||||
|
self.assertEqual(client.cookies["remember_provider"]["max-age"], 0)
|
||||||
|
|
||||||
|
def test_renew(self):
|
||||||
|
"""
|
||||||
|
Test authentication renewal with federation mode
|
||||||
|
"""
|
||||||
|
tickets = self.test_login_post_provider()
|
||||||
|
for (provider, _, client) in tickets:
|
||||||
|
# Try to renew authentication(client already authenticated in test_login_post_provider
|
||||||
|
response = client.get("/login?renew=true")
|
||||||
|
# we should be redirected to the user CAS
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response["Location"], "%s/federate/%s?renew=true" % (
|
||||||
|
'http://testserver' if django.VERSION < (1, 9) else "",
|
||||||
|
provider.suffix
|
||||||
|
))
|
||||||
|
|
||||||
|
response = client.get("/federate/%s?renew=true" % provider.suffix)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
service_url = (
|
||||||
|
"service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s%%3Frenew%%3Dtrue"
|
||||||
|
) % provider.suffix
|
||||||
|
self.assertIn(service_url, response["Location"])
|
||||||
|
self.assertIn("renew=true", response["Location"])
|
||||||
|
|
||||||
|
cas_port = int(provider.server_url.split(':')[-1])
|
||||||
|
# let's generate a ticket
|
||||||
|
ticket = utils.gen_st()
|
||||||
|
# we lauch a dummy CAS server that only validate once for the service
|
||||||
|
# http://testserver/federate/example.com?renew=true with `ticket`
|
||||||
|
tests_utils.DummyCAS.run(
|
||||||
|
("http://testserver/federate/%s?renew=true" % provider.suffix).encode("ascii"),
|
||||||
|
ticket.encode("ascii"),
|
||||||
|
settings.CAS_TEST_USER.encode("utf8"),
|
||||||
|
[],
|
||||||
|
cas_port
|
||||||
|
)
|
||||||
|
# we normally provide a good ticket and should be redirected to /login as the ticket
|
||||||
|
# get successfully validated again the dummy CAS
|
||||||
|
response = client.get(
|
||||||
|
'/federate/%s' % provider.suffix,
|
||||||
|
{'ticket': ticket, 'renew': 'true'}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response["Location"], "%s/login?renew=true" % (
|
||||||
|
'http://testserver' if django.VERSION < (1, 9) else ""
|
||||||
|
))
|
||||||
|
# follow the redirect and try to get a ticket to see is it has renew set to True
|
||||||
|
response = client.get("/login?renew=true&service=%s" % self.service)
|
||||||
|
# we should get a page with a from with all widget hidden that auto POST to /login using
|
||||||
|
# javascript. If javascript is disabled, a "connect" button is showed
|
||||||
|
self.assertTrue(response.context['auto_submit'])
|
||||||
|
self.assertEqual(response.context['post_url'], '/login')
|
||||||
|
params = tests_utils.copy_form(response.context["form"])
|
||||||
|
# POST get prefiled from parameters
|
||||||
|
response = client.post("/login", params)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertTrue(response["Location"].startswith("%s?ticket=" % self.service))
|
||||||
|
ticket_value = response["Location"].split('ticket=')[-1]
|
||||||
|
ticket = models.ServiceTicket.objects.get(value=ticket_value)
|
||||||
|
self.assertTrue(ticket.renew)
|
||||||
|
|
||||||
def test_login_bad_ticket(self):
|
def test_login_bad_ticket(self):
|
||||||
"""
|
"""
|
||||||
Try login with a bad ticket:
|
Try login with a bad ticket:
|
||||||
|
@ -16,7 +16,9 @@ import django
|
|||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core import mail
|
||||||
|
|
||||||
|
import mock
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
@ -60,6 +62,25 @@ class FederatedUserTestCase(TestCase, UserModels, FederatedIendityProviderModel)
|
|||||||
with self.assertRaises(models.FederatedUser.DoesNotExist):
|
with self.assertRaises(models.FederatedUser.DoesNotExist):
|
||||||
models.FederatedUser.objects.get(username="test2")
|
models.FederatedUser.objects.get(username="test2")
|
||||||
|
|
||||||
|
def test_json_attributes(self):
|
||||||
|
"""test the json storage of ``atrributs`` in ``_attributs``"""
|
||||||
|
provider = models.FederatedIendityProvider.objects.get(suffix="example.com")
|
||||||
|
user = models.FederatedUser.objects.create(
|
||||||
|
username=settings.CAS_TEST_USER,
|
||||||
|
provider=provider,
|
||||||
|
attributs=settings.CAS_TEST_ATTRIBUTES,
|
||||||
|
ticket=""
|
||||||
|
)
|
||||||
|
self.assertEqual(utils.json_encode(settings.CAS_TEST_ATTRIBUTES), user._attributs)
|
||||||
|
user.delete()
|
||||||
|
user = models.FederatedUser.objects.create(
|
||||||
|
username=settings.CAS_TEST_USER,
|
||||||
|
provider=provider,
|
||||||
|
ticket=""
|
||||||
|
)
|
||||||
|
self.assertIsNone(user._attributs)
|
||||||
|
self.assertIsNone(user.attributs)
|
||||||
|
|
||||||
|
|
||||||
class FederateSLOTestCase(TestCase, UserModels):
|
class FederateSLOTestCase(TestCase, UserModels):
|
||||||
"""test for the federated SLO model"""
|
"""test for the federated SLO model"""
|
||||||
@ -231,3 +252,65 @@ class TicketTestCase(TestCase, UserModels, BaseServicePattern):
|
|||||||
self.assertTrue(b'logoutRequest' in params and params[b'logoutRequest'])
|
self.assertTrue(b'logoutRequest' in params and params[b'logoutRequest'])
|
||||||
# only 1 ticket remain in the db
|
# only 1 ticket remain in the db
|
||||||
self.assertEqual(len(models.ServiceTicket.objects.all()), 1)
|
self.assertEqual(len(models.ServiceTicket.objects.all()), 1)
|
||||||
|
|
||||||
|
def test_json_attributes(self):
|
||||||
|
"""test the json storage of ``atrributs`` in ``_attributs``"""
|
||||||
|
# ge an authenticated client
|
||||||
|
client = get_auth_client()
|
||||||
|
# get the user associated to the client
|
||||||
|
user = self.get_user(client)
|
||||||
|
ticket = models.ServiceTicket.objects.create(
|
||||||
|
user=user,
|
||||||
|
service=self.service,
|
||||||
|
attributs=settings.CAS_TEST_ATTRIBUTES,
|
||||||
|
service_pattern=self.service_pattern
|
||||||
|
)
|
||||||
|
self.assertEqual(utils.json_encode(settings.CAS_TEST_ATTRIBUTES), ticket._attributs)
|
||||||
|
ticket.delete()
|
||||||
|
ticket = models.ServiceTicket.objects.create(
|
||||||
|
user=user,
|
||||||
|
service=self.service,
|
||||||
|
service_pattern=self.service_pattern
|
||||||
|
)
|
||||||
|
self.assertIsNone(ticket._attributs)
|
||||||
|
self.assertIsNone(ticket.attributs)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("cas_server.utils.last_version", lambda: "1.2.3")
|
||||||
|
@override_settings(ADMINS=[("Ano Nymous", "ano.nymous@example.net")])
|
||||||
|
@override_settings(CAS_NEW_VERSION_EMAIL_WARNING=True)
|
||||||
|
class NewVersionWarningTestCase(TestCase):
|
||||||
|
"""tests for the new version warning model"""
|
||||||
|
|
||||||
|
@mock.patch("cas_server.models.VERSION", "0.1.2")
|
||||||
|
def test_send_mails(self):
|
||||||
|
"""test the send_mails method with ADMINS and a new version available"""
|
||||||
|
models.NewVersionWarning.send_mails()
|
||||||
|
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
mail.outbox[0].subject,
|
||||||
|
'%sA new version of django-cas-server is available' % settings.EMAIL_SUBJECT_PREFIX
|
||||||
|
)
|
||||||
|
|
||||||
|
models.NewVersionWarning.send_mails()
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
@mock.patch("cas_server.models.VERSION", "1.2.3")
|
||||||
|
def test_send_mails_same_version(self):
|
||||||
|
"""test the send_mails method with with current version being the last"""
|
||||||
|
models.NewVersionWarning.objects.create(version="0.1.2")
|
||||||
|
models.NewVersionWarning.send_mails()
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
@override_settings(ADMINS=[])
|
||||||
|
def test_send_mails_no_admins(self):
|
||||||
|
"""test the send_mails method without ADMINS"""
|
||||||
|
models.NewVersionWarning.send_mails()
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
@override_settings(CAS_NEW_VERSION_EMAIL_WARNING=False)
|
||||||
|
def test_send_mails_disabled(self):
|
||||||
|
"""test the send_mails method if disabled"""
|
||||||
|
models.NewVersionWarning.send_mails()
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
@ -11,8 +11,11 @@
|
|||||||
# (c) 2016 Valentin Samir
|
# (c) 2016 Valentin Samir
|
||||||
"""Tests module for utils"""
|
"""Tests module for utils"""
|
||||||
from django.test import TestCase, RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
import warnings
|
||||||
|
import datetime
|
||||||
|
|
||||||
from cas_server import utils
|
from cas_server import utils
|
||||||
|
|
||||||
@ -128,16 +131,23 @@ class CheckPasswordCase(TestCase):
|
|||||||
with self.assertRaises(utils.LdapHashUserPassword.BadHash):
|
with self.assertRaises(utils.LdapHashUserPassword.BadHash):
|
||||||
utils.check_password("ldap", self.password1, b"TOTOssdsdsd", "utf8")
|
utils.check_password("ldap", self.password1, b"TOTOssdsdsd", "utf8")
|
||||||
for scheme in schemes_salt:
|
for scheme in schemes_salt:
|
||||||
|
# bad length
|
||||||
with self.assertRaises(utils.LdapHashUserPassword.BadHash):
|
with self.assertRaises(utils.LdapHashUserPassword.BadHash):
|
||||||
utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw==", "utf8")
|
utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw==", "utf8")
|
||||||
|
# bad base64
|
||||||
|
with self.assertRaises(utils.LdapHashUserPassword.BadHash):
|
||||||
|
utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw", "utf8")
|
||||||
|
|
||||||
def test_hex(self):
|
def test_hex(self):
|
||||||
"""test all the hex_HASH method: the hashed password is a simple hash of the password"""
|
"""test all the hex_HASH method: the hashed password is a simple hash of the password"""
|
||||||
hashes = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]
|
hashes = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]
|
||||||
hashed_password1 = []
|
hashed_password1 = []
|
||||||
for hash in hashes:
|
for hash_scheme in hashes:
|
||||||
hashed_password1.append(
|
hashed_password1.append(
|
||||||
("hex_%s" % hash, getattr(utils.hashlib, hash)(self.password1).hexdigest())
|
(
|
||||||
|
"hex_%s" % hash_scheme,
|
||||||
|
getattr(utils.hashlib, hash_scheme)(self.password1).hexdigest()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for (method, hp1) in hashed_password1:
|
for (method, hp1) in hashed_password1:
|
||||||
self.assertTrue(utils.check_password(method, self.password1, hp1, "utf8"))
|
self.assertTrue(utils.check_password(method, self.password1, hp1, "utf8"))
|
||||||
@ -208,3 +218,40 @@ class UtilsTestCase(TestCase):
|
|||||||
self.assertEqual(utils.get_tuple(test_tuple, 3), None)
|
self.assertEqual(utils.get_tuple(test_tuple, 3), None)
|
||||||
self.assertEqual(utils.get_tuple(test_tuple, 3, 'toto'), 'toto')
|
self.assertEqual(utils.get_tuple(test_tuple, 3, 'toto'), 'toto')
|
||||||
self.assertEqual(utils.get_tuple(None, 3), None)
|
self.assertEqual(utils.get_tuple(None, 3), None)
|
||||||
|
|
||||||
|
def test_last_version(self):
|
||||||
|
"""
|
||||||
|
test the function last_version. An internet connection is needed, if you do not have
|
||||||
|
one, this test will fail and you should ignore it.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# first check if pypi is available
|
||||||
|
utils.requests.get("https://pypi.python.org/simple/django-cas-server/")
|
||||||
|
except utils.requests.exceptions.RequestException:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"Pypi seems not available, perhaps you do not have internet access. "
|
||||||
|
"Consequently, the test cas_server.tests.test_utils.UtilsTestCase.test_last_"
|
||||||
|
"version is ignored"
|
||||||
|
),
|
||||||
|
RuntimeWarning
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
version = utils.last_version()
|
||||||
|
self.assertIsInstance(version, six.text_type)
|
||||||
|
self.assertEqual(len(version.split('.')), 3)
|
||||||
|
|
||||||
|
# version is cached 24h so calling it a second time should return the save value
|
||||||
|
self.assertEqual(version, utils.last_version())
|
||||||
|
|
||||||
|
def test_dictfetchall(self):
|
||||||
|
"""test the function dictfetchall"""
|
||||||
|
with connection.cursor() as curs:
|
||||||
|
curs.execute("SELECT * FROM django_migrations")
|
||||||
|
results = utils.dictfetchall(curs)
|
||||||
|
self.assertIsInstance(results, list)
|
||||||
|
self.assertTrue(len(results) > 0)
|
||||||
|
for result in results:
|
||||||
|
self.assertIsInstance(result, dict)
|
||||||
|
self.assertIn('applied', result)
|
||||||
|
self.assertIsInstance(result['applied'], datetime.datetime)
|
||||||
|
@ -20,6 +20,7 @@ from django.utils import timezone
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
|
import mock
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from six.moves import range
|
from six.moves import range
|
||||||
|
|
||||||
@ -47,6 +48,33 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
|
|||||||
# we prepare a bunch a service url and service patterns for tests
|
# we prepare a bunch a service url and service patterns for tests
|
||||||
self.setup_service_patterns()
|
self.setup_service_patterns()
|
||||||
|
|
||||||
|
@override_settings(CAS_NEW_VERSION_HTML_WARNING=True)
|
||||||
|
@mock.patch("cas_server.utils.last_version", lambda: "1.2.3")
|
||||||
|
@mock.patch("cas_server.utils.VERSION", "0.1.2")
|
||||||
|
def test_new_version_available_ok(self):
|
||||||
|
"""test the new version info box"""
|
||||||
|
client = Client()
|
||||||
|
response = client.get("/login")
|
||||||
|
self.assertIn(b"A new version of the application is available", response.content)
|
||||||
|
|
||||||
|
@override_settings(CAS_NEW_VERSION_HTML_WARNING=True)
|
||||||
|
@mock.patch("cas_server.utils.last_version", lambda: None)
|
||||||
|
@mock.patch("cas_server.utils.VERSION", "0.1.2")
|
||||||
|
def test_new_version_available_badpypi(self):
|
||||||
|
"""
|
||||||
|
test the new version info box if pypi is not available (unable to retreive last version)
|
||||||
|
"""
|
||||||
|
client = Client()
|
||||||
|
response = client.get("/login")
|
||||||
|
self.assertNotIn(b"A new version of the application is available", response.content)
|
||||||
|
|
||||||
|
@override_settings(CAS_NEW_VERSION_HTML_WARNING=False)
|
||||||
|
def test_new_version_available_disabled(self):
|
||||||
|
"""test the new version info box is disabled"""
|
||||||
|
client = Client()
|
||||||
|
response = client.get("/login")
|
||||||
|
self.assertNotIn(b"A new version of the application is available", response.content)
|
||||||
|
|
||||||
def test_login_view_post_goodpass_goodlt(self):
|
def test_login_view_post_goodpass_goodlt(self):
|
||||||
"""Test a successul login"""
|
"""Test a successul login"""
|
||||||
# we get a client who fetch a frist time the login page and the login form default
|
# we get a client who fetch a frist time the login page and the login form default
|
||||||
@ -309,7 +337,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
|
|||||||
response = client.get("/login", {'service': service})
|
response = client.get("/login", {'service': service})
|
||||||
# the ticket is not created and a warning is displayed to the user
|
# the ticket is not created and a warning is displayed to the user
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(b"User charateristics non allowed" in response.content)
|
self.assertTrue(b"User characteristics non allowed" in response.content)
|
||||||
|
|
||||||
# same but with rectriction that a valid upon the test user attributes
|
# same but with rectriction that a valid upon the test user attributes
|
||||||
response = client.get("/login", {'service': self.service_filter_success})
|
response = client.get("/login", {'service': self.service_filter_success})
|
||||||
@ -327,7 +355,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
|
|||||||
response = client.get("/login", {'service': self.service_field_needed_fail})
|
response = client.get("/login", {'service': self.service_field_needed_fail})
|
||||||
# the ticket is not created and a warning is displayed to the user
|
# the ticket is not created and a warning is displayed to the user
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(b"The attribut uid is needed to use that service" in response.content)
|
self.assertTrue(b"The attribute uid is needed to use that service" in response.content)
|
||||||
|
|
||||||
# same but with a attribute that the test user has
|
# same but with a attribute that the test user has
|
||||||
response = client.get("/login", {'service': self.service_field_needed_success})
|
response = client.get("/login", {'service': self.service_field_needed_success})
|
||||||
@ -351,7 +379,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
|
|||||||
response = client.get("/login", {"service": self.service_field_needed_success})
|
response = client.get("/login", {"service": self.service_field_needed_success})
|
||||||
# the ticket is not created and a warning is displayed to the user
|
# the ticket is not created and a warning is displayed to the user
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(b"The attribut alias is needed to use that service" in response.content)
|
self.assertTrue(b"The attribute alias is needed to use that service" in response.content)
|
||||||
|
|
||||||
def test_gateway(self):
|
def test_gateway(self):
|
||||||
"""test gateway parameter"""
|
"""test gateway parameter"""
|
||||||
|
@ -12,8 +12,9 @@
|
|||||||
"""Some utils functions for tests"""
|
"""Some utils functions for tests"""
|
||||||
from cas_server.default_settings import settings
|
from cas_server.default_settings import settings
|
||||||
|
|
||||||
|
import django
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.template import loader, Context
|
from django.template import loader
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
import cgi
|
import cgi
|
||||||
@ -21,13 +22,25 @@ import six
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from six.moves import BaseHTTPServer
|
from six.moves import BaseHTTPServer
|
||||||
from six.moves.urllib.parse import urlparse, parse_qsl
|
from six.moves.urllib.parse import urlparse, parse_qsl, parse_qs
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from cas_server import models
|
from cas_server import models
|
||||||
from cas_server import utils
|
from cas_server import utils
|
||||||
|
|
||||||
|
|
||||||
|
if django.VERSION < (1, 8):
|
||||||
|
from django.template import Context
|
||||||
|
else:
|
||||||
|
def Context(arg):
|
||||||
|
"""
|
||||||
|
Starting from django 1.8 render take a dict and deprecated the use of a Context.
|
||||||
|
So this is the identity function, only use for compatibility with django 1.7 where
|
||||||
|
render MUST take a Context as argument.
|
||||||
|
"""
|
||||||
|
return arg
|
||||||
|
|
||||||
|
|
||||||
def return_unicode(string, charset):
|
def return_unicode(string, charset):
|
||||||
"""make `string` a unicode if `string` is a unicode or bytes encoded with `charset`"""
|
"""make `string` a unicode if `string` is a unicode or bytes encoded with `charset`"""
|
||||||
if not isinstance(string, six.text_type):
|
if not isinstance(string, six.text_type):
|
||||||
@ -166,7 +179,7 @@ class HttpParamsHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|||||||
postvars = cgi.parse_multipart(self.rfile, pdict)
|
postvars = cgi.parse_multipart(self.rfile, pdict)
|
||||||
elif ctype == 'application/x-www-form-urlencoded':
|
elif ctype == 'application/x-www-form-urlencoded':
|
||||||
length = int(self.headers.get('content-length'))
|
length = int(self.headers.get('content-length'))
|
||||||
postvars = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1)
|
postvars = parse_qs(self.rfile.read(length), keep_blank_values=1)
|
||||||
else:
|
else:
|
||||||
postvars = {}
|
postvars = {}
|
||||||
self.server.PARAMS = postvars
|
self.server.PARAMS = postvars
|
||||||
|
@ -16,8 +16,10 @@ from django.views.decorators.debug import sensitive_post_parameters, sensitive_v
|
|||||||
|
|
||||||
from cas_server import views
|
from cas_server import views
|
||||||
|
|
||||||
|
app_name = "cas_server"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', RedirectView.as_view(pattern_name="cas_server:login")),
|
url(r'^$', RedirectView.as_view(pattern_name="cas_server:login", permanent=False)),
|
||||||
url(
|
url(
|
||||||
'^login$',
|
'^login$',
|
||||||
sensitive_post_parameters('password')(
|
sensitive_post_parameters('password')(
|
||||||
@ -51,8 +53,8 @@ urlpatterns = [
|
|||||||
url('^samlValidate$', views.SamlValidate.as_view(), name='samlValidate'),
|
url('^samlValidate$', views.SamlValidate.as_view(), name='samlValidate'),
|
||||||
url(
|
url(
|
||||||
'^auth$',
|
'^auth$',
|
||||||
sensitive_variables('password')(
|
sensitive_variables('password', 'secret')(
|
||||||
sensitive_post_parameters('password')(
|
sensitive_post_parameters('password', 'secret')(
|
||||||
views.Auth.as_view()
|
views.Auth.as_view()
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -25,11 +25,20 @@ import hashlib
|
|||||||
import crypt
|
import crypt
|
||||||
import base64
|
import base64
|
||||||
import six
|
import six
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from datetime import datetime, timedelta
|
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
|
||||||
|
|
||||||
|
from . import VERSION
|
||||||
|
|
||||||
|
#: logger facility
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def json_encode(obj):
|
def json_encode(obj):
|
||||||
"""Encode a python object to json"""
|
"""Encode a python object to json"""
|
||||||
@ -51,6 +60,14 @@ def context(params):
|
|||||||
"""
|
"""
|
||||||
params["settings"] = settings
|
params["settings"] = settings
|
||||||
params["message_levels"] = DEFAULT_MESSAGE_LEVELS
|
params["message_levels"] = DEFAULT_MESSAGE_LEVELS
|
||||||
|
if settings.CAS_NEW_VERSION_HTML_WARNING:
|
||||||
|
LAST_VERSION = last_version()
|
||||||
|
params["VERSION"] = VERSION
|
||||||
|
params["LAST_VERSION"] = LAST_VERSION
|
||||||
|
if LAST_VERSION is not None:
|
||||||
|
params["upgrade_available"] = decode_version(VERSION) < decode_version(LAST_VERSION)
|
||||||
|
else:
|
||||||
|
params["upgrade_available"] = False
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
@ -545,7 +562,10 @@ class LdapHashUserPassword(object):
|
|||||||
elif scheme == b'{CRYPT}':
|
elif scheme == b'{CRYPT}':
|
||||||
return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):]
|
return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):]
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
hashed_passord = base64.b64decode(hashed_passord[len(scheme):])
|
hashed_passord = base64.b64decode(hashed_passord[len(scheme):])
|
||||||
|
except (TypeError, binascii.Error) as error:
|
||||||
|
raise cls.BadHash("Bad base64: %s" % error)
|
||||||
if len(hashed_passord) < cls._schemes_to_len[scheme]:
|
if len(hashed_passord) < cls._schemes_to_len[scheme]:
|
||||||
raise cls.BadHash("Hash too short for the scheme %s" % scheme)
|
raise cls.BadHash("Hash too short for the scheme %s" % scheme)
|
||||||
return hashed_passord[cls._schemes_to_len[scheme]:]
|
return hashed_passord[cls._schemes_to_len[scheme]:]
|
||||||
@ -563,7 +583,7 @@ def check_password(method, password, hashed_password, charset):
|
|||||||
:param hashed_password: The hashed password as stored in the database
|
:param hashed_password: The hashed password as stored in the database
|
||||||
:type hashed_password: :obj:`str` or :obj:`unicode`
|
:type hashed_password: :obj:`str` or :obj:`unicode`
|
||||||
:param str charset: The used char encoding (also used internally, so it must be valid for
|
: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`)
|
the charset used by ``password`` when it was initially )
|
||||||
:return: True if ``password`` match ``hashed_password`` using ``method``,
|
:return: True if ``password`` match ``hashed_password`` using ``method``,
|
||||||
``False`` otherwise
|
``False`` otherwise
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
@ -603,3 +623,60 @@ def check_password(method, password, hashed_password, charset):
|
|||||||
)(password).hexdigest().encode("ascii") == hashed_password.lower()
|
)(password).hexdigest().encode("ascii") == hashed_password.lower()
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown password method check %r" % method)
|
raise ValueError("Unknown password method check %r" % method)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_version(version):
|
||||||
|
"""
|
||||||
|
decode a version string following version semantic http://semver.org/ input a tuple of int
|
||||||
|
|
||||||
|
:param unicode version: A dotted version
|
||||||
|
:return: A tuple a int
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
|
return tuple(int(sub_version) for sub_version in version.split('.'))
|
||||||
|
|
||||||
|
|
||||||
|
def last_version():
|
||||||
|
"""
|
||||||
|
Fetch the last version from pypi and return it. On successful fetch from pypi, the response
|
||||||
|
is cached 24h, on error, it is cached 10 min.
|
||||||
|
|
||||||
|
:return: the last django-cas-server version
|
||||||
|
:rtype: unicode
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
last_update, version, success = last_version._cache
|
||||||
|
except AttributeError:
|
||||||
|
last_update = 0
|
||||||
|
version = None
|
||||||
|
success = False
|
||||||
|
cache_delta = 24 * 3600 if success else 600
|
||||||
|
if (time.time() - last_update) < cache_delta:
|
||||||
|
return version
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
req = requests.get(settings.CAS_NEW_VERSION_JSON_URL)
|
||||||
|
data = json.loads(req.text)
|
||||||
|
versions = list(data["releases"].keys())
|
||||||
|
versions.sort()
|
||||||
|
version = versions[-1]
|
||||||
|
last_version._cache = (time.time(), version, True)
|
||||||
|
return version
|
||||||
|
except (
|
||||||
|
KeyError,
|
||||||
|
ValueError,
|
||||||
|
requests.exceptions.RequestException
|
||||||
|
) as error: # pragma: no cover (should not happen unless pypi is not available)
|
||||||
|
logger.error(
|
||||||
|
"Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error)
|
||||||
|
)
|
||||||
|
last_version._cache = (time.time(), version, False)
|
||||||
|
|
||||||
|
|
||||||
|
def dictfetchall(cursor):
|
||||||
|
"Return all rows from a django cursor as a dict"
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
return [
|
||||||
|
dict(zip(columns, row))
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
@ -147,9 +147,12 @@ class LogoutView(View, LogoutMixin):
|
|||||||
# current querystring
|
# current querystring
|
||||||
if settings.CAS_FEDERATE:
|
if settings.CAS_FEDERATE:
|
||||||
if auth is not None:
|
if auth is not None:
|
||||||
params = utils.copy_params(request.GET)
|
params = utils.copy_params(request.GET, ignore={"forget_provider"})
|
||||||
url = auth.get_logout_url()
|
url = auth.get_logout_url()
|
||||||
return HttpResponseRedirect(utils.update_url(url, params))
|
response = HttpResponseRedirect(utils.update_url(url, params))
|
||||||
|
if request.GET.get("forget_provider"):
|
||||||
|
response.delete_cookie("remember_provider")
|
||||||
|
return response
|
||||||
# if service is set, redirect to service after logout
|
# if service is set, redirect to service after logout
|
||||||
if self.service:
|
if self.service:
|
||||||
list(messages.get_messages(request)) # clean messages before leaving the django app
|
list(messages.get_messages(request)) # clean messages before leaving the django app
|
||||||
@ -209,6 +212,7 @@ class LogoutView(View, LogoutMixin):
|
|||||||
|
|
||||||
class FederateAuth(View):
|
class FederateAuth(View):
|
||||||
"""view to authenticated user agains a backend CAS then CAS_FEDERATE is True"""
|
"""view to authenticated user agains a backend CAS then CAS_FEDERATE is True"""
|
||||||
|
|
||||||
@method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception
|
@method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -218,8 +222,7 @@ class FederateAuth(View):
|
|||||||
"""
|
"""
|
||||||
return super(FederateAuth, self).dispatch(request, *args, **kwargs)
|
return super(FederateAuth, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
def get_cas_client(self, request, provider, renew=False):
|
||||||
def get_cas_client(request, provider):
|
|
||||||
"""
|
"""
|
||||||
return a CAS client object matching provider
|
return a CAS client object matching provider
|
||||||
|
|
||||||
@ -231,7 +234,8 @@ class FederateAuth(View):
|
|||||||
"""
|
"""
|
||||||
# compute the current url, ignoring ticket dans provider GET parameters
|
# compute the current url, ignoring ticket dans provider GET parameters
|
||||||
service_url = utils.get_current_url(request, {"ticket", "provider"})
|
service_url = utils.get_current_url(request, {"ticket", "provider"})
|
||||||
return CASFederateValidateUser(provider, service_url)
|
self.service_url = service_url
|
||||||
|
return CASFederateValidateUser(provider, service_url, renew=renew)
|
||||||
|
|
||||||
def post(self, request, provider=None):
|
def post(self, request, provider=None):
|
||||||
"""
|
"""
|
||||||
@ -264,24 +268,16 @@ class FederateAuth(View):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
params = utils.copy_params(
|
params = utils.copy_params(
|
||||||
request.POST,
|
request.POST,
|
||||||
ignore={"provider", "csrfmiddlewaretoken", "ticket"}
|
ignore={"provider", "csrfmiddlewaretoken", "ticket", "lt"}
|
||||||
)
|
)
|
||||||
|
if params.get("renew") == "False":
|
||||||
|
del params["renew"]
|
||||||
url = utils.reverse_params(
|
url = utils.reverse_params(
|
||||||
"cas_server:federateAuth",
|
"cas_server:federateAuth",
|
||||||
kwargs=dict(provider=form.cleaned_data["provider"].suffix),
|
kwargs=dict(provider=form.cleaned_data["provider"].suffix),
|
||||||
params=params
|
params=params
|
||||||
)
|
)
|
||||||
response = HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
# If the user has checked "remember my identity provider" store it in a cookie
|
|
||||||
if form.cleaned_data["remember"]:
|
|
||||||
max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT
|
|
||||||
utils.set_cookie(
|
|
||||||
response,
|
|
||||||
"_remember_provider",
|
|
||||||
form.cleaned_data["provider"].suffix,
|
|
||||||
max_age
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
else:
|
else:
|
||||||
return redirect("cas_server:login")
|
return redirect("cas_server:login")
|
||||||
|
|
||||||
@ -296,22 +292,24 @@ class FederateAuth(View):
|
|||||||
if not settings.CAS_FEDERATE:
|
if not settings.CAS_FEDERATE:
|
||||||
logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode")
|
logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode")
|
||||||
return redirect("cas_server:login")
|
return redirect("cas_server:login")
|
||||||
|
renew = bool(request.GET.get('renew') and request.GET['renew'] != "False")
|
||||||
# Is the user is already authenticated, no need to request authentication to the user
|
# Is the user is already authenticated, no need to request authentication to the user
|
||||||
# identity provider.
|
# identity provider.
|
||||||
if self.request.session.get("authenticated"):
|
if self.request.session.get("authenticated") and not renew:
|
||||||
logger.warning("User already authenticated, dropping federate authentication request")
|
logger.warning("User already authenticated, dropping federate authentication request")
|
||||||
return redirect("cas_server:login")
|
return redirect("cas_server:login")
|
||||||
try:
|
try:
|
||||||
# get the identity provider from its suffix
|
# get the identity provider from its suffix
|
||||||
provider = FederatedIendityProvider.objects.get(suffix=provider)
|
provider = FederatedIendityProvider.objects.get(suffix=provider)
|
||||||
# get a CAS client for the user identity provider
|
# get a CAS client for the user identity provider
|
||||||
auth = self.get_cas_client(request, provider)
|
auth = self.get_cas_client(request, provider, renew)
|
||||||
# if no ticket submited, redirect to the identity provider CAS login page
|
# if no ticket submited, redirect to the identity provider CAS login page
|
||||||
if 'ticket' not in request.GET:
|
if 'ticket' not in request.GET:
|
||||||
logger.info("Trying to authenticate again %s" % auth.provider.server_url)
|
logger.info("Trying to authenticate again %s" % auth.provider.server_url)
|
||||||
return HttpResponseRedirect(auth.get_login_url())
|
return HttpResponseRedirect(auth.get_login_url())
|
||||||
else:
|
else:
|
||||||
ticket = request.GET['ticket']
|
ticket = request.GET['ticket']
|
||||||
|
try:
|
||||||
# if the ticket validation succeed
|
# if the ticket validation succeed
|
||||||
if auth.verify_ticket(ticket):
|
if auth.verify_ticket(ticket):
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -320,23 +318,55 @@ class FederateAuth(View):
|
|||||||
auth.provider.server_url
|
auth.provider.server_url
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
params = utils.copy_params(request.GET, ignore={"ticket"})
|
params = utils.copy_params(request.GET, ignore={"ticket", "remember"})
|
||||||
request.session["federate_username"] = auth.federated_username
|
request.session["federate_username"] = auth.federated_username
|
||||||
request.session["federate_ticket"] = ticket
|
request.session["federate_ticket"] = ticket
|
||||||
auth.register_slo(auth.federated_username, request.session.session_key, ticket)
|
auth.register_slo(
|
||||||
|
auth.federated_username,
|
||||||
|
request.session.session_key,
|
||||||
|
ticket
|
||||||
|
)
|
||||||
# redirect to the the login page for the user to become authenticated
|
# redirect to the the login page for the user to become authenticated
|
||||||
# thanks to the `federate_username` and `federate_ticket` session parameters
|
# thanks to the `federate_username` and `federate_ticket` session parameters
|
||||||
url = utils.reverse_params("cas_server:login", params)
|
url = utils.reverse_params("cas_server:login", params)
|
||||||
return HttpResponseRedirect(url)
|
response = HttpResponseRedirect(url)
|
||||||
|
# If the user has checked "remember my identity provider" store it in a
|
||||||
|
# cookie
|
||||||
|
if request.GET.get("remember"):
|
||||||
|
max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT
|
||||||
|
utils.set_cookie(
|
||||||
|
response,
|
||||||
|
"remember_provider",
|
||||||
|
provider.suffix,
|
||||||
|
max_age
|
||||||
|
)
|
||||||
|
return response
|
||||||
# else redirect to the identity provider CAS login page
|
# else redirect to the identity provider CAS login page
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Got a invalid ticket for %s from %s. Retrying to authenticate" % (
|
(
|
||||||
auth.username,
|
"Got a invalid ticket %s from %s for service %s. "
|
||||||
auth.provider.server_url
|
"Retrying to authenticate"
|
||||||
|
) % (
|
||||||
|
ticket,
|
||||||
|
auth.provider.server_url,
|
||||||
|
self.service_url
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(auth.get_login_url())
|
return HttpResponseRedirect(auth.get_login_url())
|
||||||
|
# both xml.etree.ElementTree and lxml.etree exceptions inherit from SyntaxError
|
||||||
|
except SyntaxError as error:
|
||||||
|
messages.add_message(
|
||||||
|
request,
|
||||||
|
messages.ERROR,
|
||||||
|
_(
|
||||||
|
u"Invalid response from your identity provider CAS upon "
|
||||||
|
u"ticket %(ticket)s validation: %(error)r"
|
||||||
|
) % {'ticket': ticket, 'error': error}
|
||||||
|
)
|
||||||
|
response = redirect("cas_server:login")
|
||||||
|
response.delete_cookie("remember_provider")
|
||||||
|
return response
|
||||||
except FederatedIendityProvider.DoesNotExist:
|
except FederatedIendityProvider.DoesNotExist:
|
||||||
logger.warning("Identity provider suffix %s not found" % provider)
|
logger.warning("Identity provider suffix %s not found" % provider)
|
||||||
# if the identity provider is not found, redirect to the login page
|
# if the identity provider is not found, redirect to the login page
|
||||||
@ -407,7 +437,8 @@ class LoginView(View, LogoutMixin):
|
|||||||
self.warn = request.POST.get('warn')
|
self.warn = request.POST.get('warn')
|
||||||
if settings.CAS_FEDERATE:
|
if settings.CAS_FEDERATE:
|
||||||
self.username = request.POST.get('username')
|
self.username = request.POST.get('username')
|
||||||
self.ticket = request.POST.get('ticket')
|
# in federated mode, the valdated indentity provider CAS ticket is used as password
|
||||||
|
self.ticket = request.POST.get('password')
|
||||||
|
|
||||||
def gen_lt(self):
|
def gen_lt(self):
|
||||||
"""Generate a new LoginTicket and add it to the list of valid LT for the user"""
|
"""Generate a new LoginTicket and add it to the list of valid LT for the user"""
|
||||||
@ -451,7 +482,7 @@ class LoginView(View, LogoutMixin):
|
|||||||
messages.add_message(
|
messages.add_message(
|
||||||
self.request,
|
self.request,
|
||||||
messages.ERROR,
|
messages.ERROR,
|
||||||
_(u"Invalid login ticket")
|
_(u"Invalid login ticket, please retry to login")
|
||||||
)
|
)
|
||||||
elif ret == self.USER_LOGIN_OK:
|
elif ret == self.USER_LOGIN_OK:
|
||||||
# On successful login, update the :class:`models.User<cas_server.models.User>` ``date``
|
# On successful login, update the :class:`models.User<cas_server.models.User>` ``date``
|
||||||
@ -477,7 +508,17 @@ class LoginView(View, LogoutMixin):
|
|||||||
else: # pragma: no cover (should no happen)
|
else: # pragma: no cover (should no happen)
|
||||||
raise EnvironmentError("invalid output for LoginView.process_post")
|
raise EnvironmentError("invalid output for LoginView.process_post")
|
||||||
# call the GET/POST common part
|
# call the GET/POST common part
|
||||||
return self.common()
|
response = self.common()
|
||||||
|
if self.warn:
|
||||||
|
utils.set_cookie(
|
||||||
|
response,
|
||||||
|
"warn",
|
||||||
|
"on",
|
||||||
|
10 * 365 * 24 * 3600
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response.delete_cookie("warn")
|
||||||
|
return response
|
||||||
|
|
||||||
def process_post(self):
|
def process_post(self):
|
||||||
"""
|
"""
|
||||||
@ -586,7 +627,9 @@ class LoginView(View, LogoutMixin):
|
|||||||
form_initial = {
|
form_initial = {
|
||||||
'service': self.service,
|
'service': self.service,
|
||||||
'method': self.method,
|
'method': self.method,
|
||||||
'warn': self.warn or self.request.session.get("warn"),
|
'warn': (
|
||||||
|
self.warn or self.request.session.get("warn") or self.request.COOKIES.get('warn')
|
||||||
|
),
|
||||||
'lt': self.request.session['lt'][-1],
|
'lt': self.request.session['lt'][-1],
|
||||||
'renew': self.renew
|
'renew': self.renew
|
||||||
}
|
}
|
||||||
@ -683,14 +726,14 @@ class LoginView(View, LogoutMixin):
|
|||||||
messages.add_message(
|
messages.add_message(
|
||||||
self.request,
|
self.request,
|
||||||
messages.ERROR,
|
messages.ERROR,
|
||||||
_(u"User charateristics non allowed")
|
_(u"User characteristics non allowed")
|
||||||
)
|
)
|
||||||
except models.UserFieldNotDefined:
|
except models.UserFieldNotDefined:
|
||||||
error = 4
|
error = 4
|
||||||
messages.add_message(
|
messages.add_message(
|
||||||
self.request,
|
self.request,
|
||||||
messages.ERROR,
|
messages.ERROR,
|
||||||
_(u"The attribut %(field)s is needed to use"
|
_(u"The attribute %(field)s is needed to use"
|
||||||
u" that service") % {'field': service_pattern.user_field}
|
u" that service") % {'field': service_pattern.user_field}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -817,19 +860,37 @@ class LoginView(View, LogoutMixin):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if (
|
if (
|
||||||
self.request.COOKIES.get('_remember_provider') and
|
self.request.COOKIES.get('remember_provider') and
|
||||||
FederatedIendityProvider.objects.filter(
|
FederatedIendityProvider.objects.filter(
|
||||||
suffix=self.request.COOKIES['_remember_provider']
|
suffix=self.request.COOKIES['remember_provider']
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
params = utils.copy_params(self.request.GET)
|
params = utils.copy_params(self.request.GET)
|
||||||
url = utils.reverse_params(
|
url = utils.reverse_params(
|
||||||
"cas_server:federateAuth",
|
"cas_server:federateAuth",
|
||||||
params=params,
|
params=params,
|
||||||
kwargs=dict(provider=self.request.COOKIES['_remember_provider'])
|
kwargs=dict(provider=self.request.COOKIES['remember_provider'])
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
else:
|
else:
|
||||||
|
# if user is authenticated and auth renewal is requested, redirect directly
|
||||||
|
# to the user identity provider
|
||||||
|
if self.renew and self.request.session.get("authenticated"):
|
||||||
|
try:
|
||||||
|
user = FederatedUser.get_from_federated_username(
|
||||||
|
self.request.session.get("username")
|
||||||
|
)
|
||||||
|
params = utils.copy_params(self.request.GET)
|
||||||
|
url = utils.reverse_params(
|
||||||
|
"cas_server:federateAuth",
|
||||||
|
params=params,
|
||||||
|
kwargs=dict(provider=user.provider.suffix)
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
# Should normally not happen: if the user is logged, it exists in the
|
||||||
|
# database.
|
||||||
|
except FederatedUser.DoesNotExist: # pragma: no cover
|
||||||
|
pass
|
||||||
return render(
|
return render(
|
||||||
self.request,
|
self.request,
|
||||||
settings.CAS_LOGIN_TEMPLATE,
|
settings.CAS_LOGIN_TEMPLATE,
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
setuptools>=5.5
|
setuptools>=5.5
|
||||||
|
requests>=2.4
|
||||||
|
requests_futures>=0.9.5
|
||||||
|
lxml>=3.4
|
||||||
|
six>=1.8
|
||||||
tox>=1.8.1
|
tox>=1.8.1
|
||||||
pytest>=2.6.4
|
pytest>=2.6.4
|
||||||
pytest-django>=2.8.0
|
pytest-django>=2.8.0
|
||||||
pytest-pythonpath>=0.3
|
pytest-pythonpath>=0.3
|
||||||
|
pytest-warnings
|
||||||
pytest-cov>=2.2.1
|
pytest-cov>=2.2.1
|
||||||
requests>=2.4
|
mock>=1
|
||||||
requests_futures>=0.9.5
|
|
||||||
lxml>=3.4
|
|
||||||
six>=1
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
Django >= 1.8,<1.10
|
Django >= 1.7.1,<1.10
|
||||||
setuptools>=5.5
|
setuptools>=5.5
|
||||||
requests>=2.4
|
requests>=2.4
|
||||||
requests_futures>=0.9.5
|
requests_futures>=0.9.5
|
||||||
lxml>=3.4
|
lxml>=3.4
|
||||||
six>=1
|
six>=1.8
|
||||||
|
7
setup.py
7
setup.py
@ -1,8 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
from cas_server import VERSION
|
||||||
VERSION = '0.6.1'
|
|
||||||
|
|
||||||
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()
|
||||||
@ -30,7 +29,7 @@ if __name__ == '__main__':
|
|||||||
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',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Framework :: Django :: 1.7',
|
'Framework :: Django :: 1.7',
|
||||||
'Framework :: Django :: 1.8',
|
'Framework :: Django :: 1.8',
|
||||||
@ -66,5 +65,5 @@ if __name__ == '__main__':
|
|||||||
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'],
|
setup_requires=['pytest-runner'],
|
||||||
tests_require=['pytest', 'pytest-django', 'pytest-pythonpath'],
|
tests_require=['pytest', 'pytest-django', 'pytest-pythonpath', 'pytest-warnings', 'mock>=1'],
|
||||||
)
|
)
|
||||||
|
2
tox.ini
2
tox.ini
@ -31,7 +31,7 @@ whitelist_externals=
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands=
|
commands=
|
||||||
py.test {posargs:cas_server/tests/}
|
py.test -rw {posargs:cas_server/tests/}
|
||||||
{[post_cmd]commands}
|
{[post_cmd]commands}
|
||||||
whitelist_externals={[post_cmd]whitelist_externals}
|
whitelist_externals={[post_cmd]whitelist_externals}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user