commit
ba59060763
36 changed files with 1255 additions and 699 deletions
|
@ -1,8 +1,6 @@
|
|||
language: python
|
||||
matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=coverage
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=flake8
|
||||
- python: "2.7"
|
||||
|
@ -23,6 +21,8 @@ matrix:
|
|||
env: TOX_ENV=py35-django18
|
||||
- python: "3.5"
|
||||
env: TOX_ENV=py35-django19
|
||||
- python: "2.7"
|
||||
env: TOX_ENV=coverage
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip/http/
|
||||
|
|
4
Makefile
4
Makefile
|
@ -38,7 +38,7 @@ dist:
|
|||
|
||||
test_venv/bin/python:
|
||||
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
|
||||
mkdir -p test_venv/cas
|
||||
|
@ -62,7 +62,7 @@ run_server: test_project
|
|||
|
||||
run_tests: test_venv
|
||||
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
|
||||
|
||||
test_venv/bin/sphinx-build: test_venv
|
||||
|
|
152
README.rst
152
README.rst
|
@ -29,11 +29,49 @@ Dependencies
|
|||
|
||||
``django-cas-server`` depends on the following python packages:
|
||||
|
||||
* Django >= 1.7 < 1.10
|
||||
* Django >= 1.7.1 < 1.10
|
||||
* requests >= 2.4
|
||||
* requests_futures >= 0.9.5
|
||||
* 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
|
||||
============
|
||||
|
@ -63,14 +101,17 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
|
|||
New python executable in cas/bin/python2
|
||||
Also creating executable in cas/bin/python
|
||||
Installing setuptools, pip...done.
|
||||
|
||||
4. And `activate it <https://virtualenv.pypa.io/en/stable/userguide/#activate-script>`__::
|
||||
|
||||
$ cd cas_venv/; . bin/activate
|
||||
|
||||
4. Create a django project::
|
||||
5. Create a django project::
|
||||
|
||||
$ django-admin startproject 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
|
||||
|
||||
|
@ -81,11 +122,11 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
|
|||
$ pip install -r requirements.txt
|
||||
|
||||
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>`_
|
||||
(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
|
||||
|
@ -145,7 +186,7 @@ Quick start
|
|||
|
||||
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
|
||||
(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.
|
||||
|
||||
|
@ -163,6 +204,8 @@ Template settings
|
|||
|
||||
* ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default
|
||||
templates. Set it to ``False`` to disable it.
|
||||
* ``CAS_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
|
||||
and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
|
||||
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
|
||||
|
@ -191,12 +234,14 @@ Template settings
|
|||
Authentication settings
|
||||
-----------------------
|
||||
|
||||
* ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing
|
||||
``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"``
|
||||
* ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing
|
||||
``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
|
||||
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
|
||||
reduce it to something like ``86400`` seconds (1 day).
|
||||
* ``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
|
||||
reduce it to something like ``86400`` seconds (1 day).
|
||||
|
||||
* ``CAS_PROXY_CA_CERTIFICATE_PATH``: Path to certificate authorities file. Usually on linux
|
||||
the local CAs are in ``/etc/ssl/certs/ca-certificates.crt``. The default is ``True`` which
|
||||
|
@ -212,13 +257,23 @@ Authentication settings
|
|||
Federation settings
|
||||
-------------------
|
||||
|
||||
* ``CAS_FEDERATE``: A boolean for activating the federated mode (see the federate section below).
|
||||
The default is ``False``.
|
||||
* ``CAS_FEDERATE``: A boolean for activating the federated mode (see the `Federation mode`_
|
||||
section below). The default is ``False``.
|
||||
* ``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
|
||||
``_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
|
||||
-------------------------
|
||||
|
||||
|
@ -257,6 +312,7 @@ Tickets miscellaneous settings
|
|||
|
||||
Mysql backend settings
|
||||
----------------------
|
||||
Deprecated, see the `Sql backend settings`_.
|
||||
Only usefull if you are using the mysql authentication backend:
|
||||
|
||||
* ``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"``.
|
||||
|
||||
|
||||
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
|
||||
---------------------
|
||||
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.
|
||||
* 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.
|
||||
* 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``.
|
||||
* 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``.
|
||||
You should not set it manually without setting ``CAS_FEDERATE`` to ``True``.
|
||||
|
||||
|
||||
Logs
|
||||
====
|
||||
|
||||
|
|
|
@ -9,5 +9,9 @@
|
|||
#
|
||||
# (c) 2015-2016 Valentin Samir
|
||||
"""A django CAS server application"""
|
||||
|
||||
#: version of the application
|
||||
VERSION = '0.6.2'
|
||||
|
||||
#: path the the application configuration class
|
||||
default_app_config = 'cas_server.apps.CasAppConfig'
|
||||
|
|
|
@ -13,16 +13,25 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from django.db import connections, DatabaseError
|
||||
|
||||
import warnings
|
||||
from datetime import timedelta
|
||||
from six.moves import range
|
||||
try: # pragma: no cover
|
||||
import MySQLdb
|
||||
import MySQLdb.cursors
|
||||
from utils import check_password
|
||||
except ImportError:
|
||||
MySQLdb = None
|
||||
|
||||
|
||||
try: # pragma: no cover
|
||||
import ldap3
|
||||
except ImportError:
|
||||
ldap3 = None
|
||||
|
||||
from .models import FederatedUser
|
||||
from .utils import check_password, dictfetchall
|
||||
|
||||
|
||||
class AuthUser(object):
|
||||
|
@ -116,19 +125,46 @@ class TestAuthUser(AuthUser):
|
|||
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):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
A mysql authentication class: authentication user agains a mysql database
|
||||
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>`
|
||||
class attribute. Valid value are fetched from the MySQL database set with
|
||||
``settings.CAS_SQL_*`` settings parameters using the query
|
||||
``settings.CAS_SQL_USER_QUERY``.
|
||||
"""
|
||||
#: Mysql user attributes as a :class:`dict` if the username is found in the database.
|
||||
user = None
|
||||
|
||||
def __init__(self, username):
|
||||
warnings.warn(
|
||||
(
|
||||
"MysqlAuthUser authentication class is deprecated: "
|
||||
"use cas_server.auth.SqlAuthUser instead"
|
||||
),
|
||||
UserWarning
|
||||
)
|
||||
# see the connect function at
|
||||
# http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes
|
||||
# for possible mysql config parameters.
|
||||
|
@ -169,24 +205,130 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
|
|||
else:
|
||||
return False
|
||||
|
||||
def attributs(self):
|
||||
"""
|
||||
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
|
||||
class SqlAuthUser(DBAuthUser): # pragma: no cover
|
||||
"""
|
||||
A SQL authentication class: authenticate user agains a SQL database. The SQL database
|
||||
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:
|
||||
return self.user
|
||||
return check_password(
|
||||
settings.CAS_SQL_PASSWORD_CHECK,
|
||||
password,
|
||||
self.user["password"],
|
||||
settings.CAS_SQL_PASSWORD_CHARSET
|
||||
)
|
||||
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
|
||||
"""
|
||||
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>`
|
||||
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("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):
|
||||
"""CAS Client Version 1"""
|
||||
|
@ -146,17 +154,15 @@ class CASClientV1(CASClientBase, ReturnUnicode):
|
|||
Returns username on success and None on failure.
|
||||
"""
|
||||
params = [('ticket', ticket), ('service', self.service_url)]
|
||||
if self.renew:
|
||||
params.append(('renew', 'true'))
|
||||
url = (urllib_parse.urljoin(self.server_url, 'validate') + '?' +
|
||||
urllib_parse.urlencode(params))
|
||||
page = urllib_request.urlopen(url)
|
||||
try:
|
||||
verified = page.readline().strip()
|
||||
if verified == b'yes':
|
||||
content_type = page.info().get('Content-type')
|
||||
if "charset=" in content_type:
|
||||
charset = content_type.split("charset=")[-1]
|
||||
else:
|
||||
charset = "ascii"
|
||||
charset = self.get_page_charset(page, default="ascii")
|
||||
user = self.u(page.readline().strip(), charset)
|
||||
return user, None, None
|
||||
else:
|
||||
|
@ -183,17 +189,15 @@ class CASClientV2(CASClientBase, ReturnUnicode):
|
|||
|
||||
def get_verification_response(self, ticket):
|
||||
params = [('ticket', ticket), ('service', self.service_url)]
|
||||
if self.renew:
|
||||
params.append(('renew', 'true'))
|
||||
if self.proxy_callback:
|
||||
params.append(('pgtUrl', self.proxy_callback))
|
||||
base_url = urllib_parse.urljoin(self.server_url, self.url_suffix)
|
||||
url = base_url + '?' + urllib_parse.urlencode(params)
|
||||
page = urllib_request.urlopen(url)
|
||||
try:
|
||||
content_type = page.info().get('Content-type')
|
||||
if "charset=" in content_type:
|
||||
charset = content_type.split("charset=")[-1]
|
||||
else:
|
||||
charset = "ascii"
|
||||
charset = self.get_page_charset(page)
|
||||
return (page.read(), charset)
|
||||
finally:
|
||||
page.close()
|
||||
|
@ -306,11 +310,7 @@ class CASClientWithSAMLV1(CASClientV2, SingleLogoutMixin):
|
|||
from elementtree import ElementTree
|
||||
|
||||
page = self.fetch_saml_validation(ticket)
|
||||
content_type = page.info().get('Content-type')
|
||||
if "charset=" in content_type:
|
||||
charset = content_type.split("charset=")[-1]
|
||||
else:
|
||||
charset = "ascii"
|
||||
charset = self.get_page_charset(page)
|
||||
|
||||
try:
|
||||
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.
|
||||
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.
|
||||
CAS_COMPONENT_URLS = {
|
||||
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
|
||||
|
@ -110,12 +112,39 @@ CAS_SQL_PASSWORD = ''
|
|||
CAS_SQL_DBNAME = ''
|
||||
#: Database charset.
|
||||
CAS_SQL_DBCHARSET = 'utf8'
|
||||
|
||||
#: The query performed upon user authentication.
|
||||
CAS_SQL_USER_QUERY = 'SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s'
|
||||
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"``,
|
||||
#: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``,
|
||||
#: ``"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.
|
||||
|
@ -140,6 +169,15 @@ CAS_FEDERATE = False
|
|||
#: Time after witch the cookie use for “remember my identity provider” expire (one week).
|
||||
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()
|
||||
for name, default_value in GLOBALS.items():
|
||||
# get the current setting value, falling back to default_value
|
||||
|
|
|
@ -42,13 +42,13 @@ class CASFederateValidateUser(object):
|
|||
#: the identity provider
|
||||
provider = None
|
||||
|
||||
def __init__(self, provider, service_url):
|
||||
def __init__(self, provider, service_url, renew=False):
|
||||
self.provider = provider
|
||||
self.client = CASClient(
|
||||
service_url=service_url,
|
||||
version=provider.cas_protocol_version,
|
||||
server_url=provider.server_url,
|
||||
renew=False,
|
||||
renew=renew,
|
||||
)
|
||||
|
||||
def get_login_url(self):
|
||||
|
|
|
@ -19,49 +19,56 @@ import cas_server.models as models
|
|||
|
||||
|
||||
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):
|
||||
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
|
||||
if not isinstance(field.widget, forms.HiddenInput):
|
||||
# tell to display the field (used in form.html)
|
||||
self[name].display = True
|
||||
attrs = {}
|
||||
if isinstance(field.widget, forms.CheckboxInput):
|
||||
self[name].checkbox = True
|
||||
else:
|
||||
if not isinstance(field.widget, forms.CheckboxInput):
|
||||
attrs['class'] = "form-control"
|
||||
if field.label:
|
||||
if field.label: # pragma: no branch (currently all field are hidden or labeled)
|
||||
attrs["placeholder"] = field.label
|
||||
if field.required:
|
||||
attrs["required"] = "required"
|
||||
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
|
||||
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 ?
|
||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
#: Url to redirect to if the authentication fail (user not authenticated or bad service)
|
||||
gateway = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
|
||||
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
|
||||
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``
|
||||
allowing the user to choose an identity provider.
|
||||
|
@ -76,39 +83,30 @@ class FederateSelect(BootsrapForm):
|
|||
to_field_name="suffix",
|
||||
label=_('Identity provider'),
|
||||
)
|
||||
#: The service url for which the user want a ticket
|
||||
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: A checkbox to ask to be warn before emiting a ticket for another service
|
||||
warn = forms.BooleanField(
|
||||
label=_('Warn me before logging me into other sites.'),
|
||||
required=False
|
||||
)
|
||||
#: A checkbox to remember the user choices of :attr:`provider<FederateSelect.provider>`
|
||||
remember = forms.BooleanField(label=_('Remember the identity provider'), required=False)
|
||||
#: A checkbox to ask to be warn before emiting a ticket for another service
|
||||
warn = forms.BooleanField(label=_('warn'), required=False)
|
||||
#: Is the service asking the authentication renewal ?
|
||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
|
||||
class UserCredential(BootsrapForm):
|
||||
class UserCredential(BaseLogin):
|
||||
"""
|
||||
Bases: :class:`django.forms.Form`
|
||||
Bases: :class:`BaseLogin`
|
||||
|
||||
Form used on the login page to retrive user credentials
|
||||
"""
|
||||
#: The user username
|
||||
username = forms.CharField(label=_('login'))
|
||||
#: The service url for which the user want a ticket
|
||||
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
|
||||
username = forms.CharField(label=_('username'))
|
||||
#: The user password
|
||||
password = forms.CharField(label=_('password'), widget=forms.PasswordInput)
|
||||
#: A valid LoginTicket to prevent POST replay
|
||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: A checkbox to ask to be warn before emiting a ticket for another service
|
||||
warn = forms.BooleanField(label=_('warn'), required=False)
|
||||
#: Is the service asking the authentication renewal ?
|
||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UserCredential, self).__init__(*args, **kwargs)
|
||||
warn = forms.BooleanField(
|
||||
label=_('Warn me before logging me into other sites.'),
|
||||
required=False
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
|
@ -124,7 +122,9 @@ class UserCredential(BootsrapForm):
|
|||
if auth.test_password(cleaned_data.get("password")):
|
||||
cleaned_data["username"] = auth.username
|
||||
else:
|
||||
raise forms.ValidationError(_(u"Bad user"))
|
||||
raise forms.ValidationError(
|
||||
_(u"The credentials you provided cannot be determined to be authentic.")
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
|
@ -148,21 +148,13 @@ class FederateUserCredential(UserCredential):
|
|||
This stub authentication form, allow to implement the federated mode with very few
|
||||
modificatons to the :class:`LoginView<cas_server.views.LoginView>` view.
|
||||
"""
|
||||
#: the user username with the ``@`` component
|
||||
username = forms.CharField(widget=forms.HiddenInput())
|
||||
#: The service url for which the user want a ticket
|
||||
service = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: The ``ticket`` used to authenticate the user against a provider
|
||||
password = forms.CharField(widget=forms.HiddenInput())
|
||||
#: alias of :attr:`password`
|
||||
ticket = forms.CharField(widget=forms.HiddenInput())
|
||||
#: A valid LoginTicket to prevent POST replay
|
||||
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
method = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
#: Has the user asked to be warn before emiting a ticket for another service
|
||||
warn = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
#: Is the service asking the authentication renewal ?
|
||||
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FederateUserCredential, self).__init__(*args, **kwargs)
|
||||
# All fields are hidden and auto filled by the /login view logic
|
||||
for name, field in self.fields.items():
|
||||
field.widget = forms.HiddenInput()
|
||||
self[name].display = False
|
||||
|
||||
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 ""
|
||||
"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:37+0200\n"
|
||||
"POT-Creation-Date: 2016-08-01 12:01+0200\n"
|
||||
"PO-Revision-Date: 2016-08-01 12:01+0200\n"
|
||||
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
|
||||
"Language-Team: django <LL@li.org>\n"
|
||||
"Language: fr\n"
|
||||
|
@ -18,45 +18,45 @@ msgstr ""
|
|||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Generator: Poedit 1.8.8\n"
|
||||
|
||||
#: apps.py:19 templates/cas_server/base.html:3
|
||||
#: templates/cas_server/base.html:20
|
||||
#: apps.py:25 templates/cas_server/base.html:9
|
||||
#: templates/cas_server/base.html:27
|
||||
msgid "Central Authentication Service"
|
||||
msgstr "Service Central d'Authentification"
|
||||
|
||||
#: forms.py:43
|
||||
#: forms.py:88
|
||||
msgid "Identity provider"
|
||||
msgstr "fournisseur d'identité"
|
||||
|
||||
#: forms.py:45 forms.py:55 forms.py:106
|
||||
msgid "service"
|
||||
msgstr "service"
|
||||
#: forms.py:92 forms.py:111
|
||||
msgid "Warn me before logging me into other sites."
|
||||
msgstr "Prévenez-moi avant d'accéder à d'autres services."
|
||||
|
||||
#: forms.py:47
|
||||
#: forms.py:96
|
||||
msgid "Remember the identity provider"
|
||||
msgstr "Se souvenir du fournisseur d'identité"
|
||||
|
||||
#: forms.py:48 forms.py:59
|
||||
msgid "warn"
|
||||
msgstr "Prévenez-moi avant d'accéder à d'autres services."
|
||||
#: forms.py:106 models.py:600
|
||||
msgid "username"
|
||||
msgstr "nom d'utilisateur"
|
||||
|
||||
#: forms.py:54
|
||||
msgid "login"
|
||||
msgstr "Identifiant"
|
||||
|
||||
#: forms.py:56
|
||||
#: forms.py:108
|
||||
msgid "password"
|
||||
msgstr "mot de passe"
|
||||
|
||||
#: forms.py:71
|
||||
msgid "Bad user"
|
||||
#: forms.py:130
|
||||
msgid "The credentials you provided cannot be determined to be authentic."
|
||||
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"
|
||||
msgstr ""
|
||||
"Utilisateur non trouvé dans la base de donnée temporaire, essayez de vous "
|
||||
"reconnecter"
|
||||
|
||||
#: forms.py:196
|
||||
msgid "service"
|
||||
msgstr "service"
|
||||
|
||||
#: management/commands/cas_clean_federate.py:20
|
||||
msgid "Clean old federated users"
|
||||
msgstr "Nettoyer les anciens utilisateurs fédéré"
|
||||
|
@ -69,98 +69,99 @@ msgstr "Nettoyer les sessions supprimées"
|
|||
msgid "Clean old trickets"
|
||||
msgstr "Nettoyer les vieux tickets"
|
||||
|
||||
#: models.py:42
|
||||
#: models.py:46
|
||||
msgid "identity provider"
|
||||
msgstr "fournisseur d'identité"
|
||||
|
||||
#: models.py:43
|
||||
#: models.py:47
|
||||
msgid "identity providers"
|
||||
msgstr "fournisseurs d'identités"
|
||||
|
||||
#: models.py:47
|
||||
#: models.py:53
|
||||
msgid "suffix"
|
||||
msgstr "suffixe"
|
||||
|
||||
#: models.py:48
|
||||
#: models.py:55
|
||||
msgid ""
|
||||
"Suffix append to backend CAS returner username: `returned_username`@`suffix`"
|
||||
"Suffix append to backend CAS returned username: ``returned_username`` @ "
|
||||
"``suffix``."
|
||||
msgstr ""
|
||||
"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"
|
||||
msgstr "url du serveur"
|
||||
|
||||
#: models.py:59
|
||||
#: models.py:72
|
||||
msgid "CAS protocol version"
|
||||
msgstr "Version du protocole CAS"
|
||||
|
||||
#: models.py:60
|
||||
#: models.py:74
|
||||
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 ""
|
||||
"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"
|
||||
msgstr "Nom du fournisseur"
|
||||
|
||||
#: models.py:66
|
||||
msgid "Name for this identity provider displayed on the login page"
|
||||
msgstr "Nom affiché pour ce fournisseur d'identité sur la page de connexion"
|
||||
#: models.py:82
|
||||
msgid "Name for this identity provider displayed on the login page."
|
||||
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"
|
||||
msgstr "position"
|
||||
|
||||
#: models.py:80
|
||||
#: models.py:102
|
||||
msgid "display"
|
||||
msgstr "afficher"
|
||||
|
||||
#: models.py:81
|
||||
msgid "Display the provider on the login page"
|
||||
msgstr "Afficher le fournisseur d'identité sur la page de connexion"
|
||||
#: models.py:103
|
||||
msgid "Display the provider on the login page."
|
||||
msgstr "Afficher le fournisseur d'identité sur la page de connexion."
|
||||
|
||||
#: models.py:164
|
||||
#: models.py:233
|
||||
msgid "User"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#: models.py:165
|
||||
#: models.py:234
|
||||
msgid "Users"
|
||||
msgstr "Utilisateurs"
|
||||
|
||||
#: models.py:234
|
||||
#: models.py:320
|
||||
#, python-format
|
||||
msgid "Error during service logout %s"
|
||||
msgstr "Une erreur est survenue durant la déconnexion du service %s"
|
||||
|
||||
#: models.py:312
|
||||
#: models.py:440
|
||||
msgid "Service pattern"
|
||||
msgstr "Motif de service"
|
||||
|
||||
#: models.py:313
|
||||
#: models.py:441
|
||||
msgid "Services patterns"
|
||||
msgstr "Motifs de services"
|
||||
|
||||
#: models.py:318
|
||||
#: models.py:447
|
||||
msgid "service patterns are sorted using the position attribute"
|
||||
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"
|
||||
msgstr "nom"
|
||||
|
||||
#: models.py:326
|
||||
#: models.py:456
|
||||
msgid "A name for the 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"
|
||||
msgstr "motif"
|
||||
|
||||
#: models.py:333
|
||||
#: models.py:466
|
||||
msgid ""
|
||||
"A regular expression matching services. Will usually looks like '^https://"
|
||||
"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 "
|
||||
"un '\\'."
|
||||
|
||||
#: models.py:342
|
||||
#: models.py:476
|
||||
msgid "user field"
|
||||
msgstr "champ utilisateur"
|
||||
|
||||
#: models.py:343
|
||||
msgid "Name of the attribut to transmit as username, empty = login"
|
||||
#: models.py:477
|
||||
msgid "Name of the attribute to transmit as username, empty = login"
|
||||
msgstr ""
|
||||
"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"
|
||||
msgstr "limiter les noms d'utilisateurs"
|
||||
|
||||
#: models.py:348
|
||||
#: models.py:483
|
||||
msgid "Limit username allowed to connect to the list provided bellow"
|
||||
msgstr ""
|
||||
"Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie "
|
||||
"ci-dessous"
|
||||
|
||||
#: models.py:352
|
||||
#: models.py:488
|
||||
msgid "proxy"
|
||||
msgstr "proxy"
|
||||
|
||||
#: models.py:353
|
||||
#: models.py:489
|
||||
msgid "Proxy tickets can be delivered to the service"
|
||||
msgstr "des proxy tickets peuvent être délivrés au service"
|
||||
|
||||
#: models.py:357
|
||||
#: models.py:495
|
||||
msgid "proxy callback"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:358
|
||||
#: models.py:496
|
||||
msgid "can be used as a proxy callback to deliver PGT"
|
||||
msgstr "peut être utilisé comme un callback pour recevoir un PGT"
|
||||
|
||||
#: models.py:362
|
||||
#: models.py:503
|
||||
msgid "single log out"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:363
|
||||
#: models.py:504
|
||||
msgid "Enable SLO for the service"
|
||||
msgstr "Active le SLO pour le service"
|
||||
|
||||
#: models.py:370
|
||||
#: models.py:512
|
||||
msgid "single log out callback"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:371
|
||||
#: models.py:513
|
||||
msgid ""
|
||||
"URL where the SLO request will be POST. empty = service url\n"
|
||||
"This is usefull for non HTTP proxied services."
|
||||
|
@ -228,83 +229,98 @@ msgstr ""
|
|||
"service\n"
|
||||
"Ceci n'est utilise que pour des services non HTTP proxifiés"
|
||||
|
||||
#: models.py:433
|
||||
msgid "username"
|
||||
msgstr "nom d'utilisateur"
|
||||
|
||||
#: models.py:434
|
||||
#: models.py:601
|
||||
msgid "username allowed to connect to the service"
|
||||
msgstr "noms d'utilisateurs autorisé à se connecter au service"
|
||||
|
||||
#: models.py:450
|
||||
msgid "name of an attribut to send to the service, use * for all attributes"
|
||||
#: models.py:627
|
||||
msgid "name of an attribute to send to the service, use * for all attributes"
|
||||
msgstr ""
|
||||
"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"
|
||||
msgstr "remplacement"
|
||||
|
||||
#: models.py:456
|
||||
#: models.py:635
|
||||
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"
|
||||
msgstr ""
|
||||
"nom sous lequel l'attribut sera rendu visible au service. vide = inchangé"
|
||||
|
||||
#: models.py:473 models.py:492
|
||||
msgid "attribut"
|
||||
#: models.py:662 models.py:692
|
||||
msgid "attribute"
|
||||
msgstr "attribut"
|
||||
|
||||
#: models.py:474
|
||||
msgid "Name of the attribut which must verify pattern"
|
||||
#: models.py:663
|
||||
msgid "Name of the attribute which must verify pattern"
|
||||
msgstr "Nom de l'attribut devant vérifier un motif"
|
||||
|
||||
#: models.py:479
|
||||
#: models.py:670
|
||||
msgid "a regular expression"
|
||||
msgstr "une expression régulière"
|
||||
|
||||
#: models.py:493
|
||||
msgid "Name of the attribut for which the value must be replace"
|
||||
msgstr "nom de l'attribue pour lequel la valeur doit être remplacé"
|
||||
#: models.py:693
|
||||
msgid "Name of the attribute for which the value must be replace"
|
||||
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"
|
||||
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 …"
|
||||
msgstr "expression de remplacement, les groupe sont capturé par \\1, \\2"
|
||||
|
||||
#: templates/cas_server/logged.html:6
|
||||
msgid "Logged"
|
||||
#: templates/cas_server/base.html:38
|
||||
#, 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 ""
|
||||
"<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."
|
||||
"Une nouvelle version de l'application est disponible. Cette instance utilise "
|
||||
"la version %(VERSION)s et la dernière version est %(LAST_VERSION)s. Merci de "
|
||||
"vous mettre a jour."
|
||||
|
||||
#: 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"
|
||||
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"
|
||||
msgstr "Se déconnecter"
|
||||
|
||||
#: templates/cas_server/login.html:8
|
||||
#: templates/cas_server/login.html:6
|
||||
msgid "Please log in"
|
||||
msgstr "Merci de se connecter"
|
||||
|
||||
#: templates/cas_server/login.html:13
|
||||
#: templates/cas_server/login.html:14
|
||||
msgid "Login"
|
||||
msgstr "Connexion"
|
||||
|
||||
#: templates/cas_server/warn.html:10
|
||||
#: templates/cas_server/warn.html:9
|
||||
msgid "Connect to the service"
|
||||
msgstr "Se connecter au service"
|
||||
|
||||
#: views.py:152
|
||||
#: views.py:168
|
||||
msgid ""
|
||||
"<h3>Logout successful</h3>You have successfully logged out from the Central "
|
||||
"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 "
|
||||
"navigateur."
|
||||
|
||||
#: views.py:158
|
||||
#: views.py:174
|
||||
#, python-format
|
||||
msgid ""
|
||||
"<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 "
|
||||
"fermer votre navigateur."
|
||||
|
||||
#: views.py:165
|
||||
#: views.py:181
|
||||
msgid ""
|
||||
"<h3>Logout successful</h3>You were already logged out from the Central "
|
||||
"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 "
|
||||
"navigateur."
|
||||
|
||||
#: views.py:349
|
||||
msgid "Invalid login ticket"
|
||||
#: views.py:361
|
||||
#, 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"
|
||||
|
||||
#: views.py:470
|
||||
#: views.py:675
|
||||
#, python-format
|
||||
msgid "Authentication has been required by service %(name)s (%(url)s)"
|
||||
msgstr ""
|
||||
"Une demande d'authentification a été émise pour le service %(name)s "
|
||||
"(%(url)s)."
|
||||
|
||||
#: views.py:508
|
||||
#: views.py:713
|
||||
#, python-format
|
||||
msgid "Service %(url)s non allowed."
|
||||
msgstr "le service %(url)s n'est pas autorisé."
|
||||
|
||||
#: views.py:515
|
||||
#: views.py:720
|
||||
msgid "Username non allowed"
|
||||
msgstr "Nom d'utilisateur non authorisé"
|
||||
|
||||
#: views.py:522
|
||||
msgid "User charateristics non allowed"
|
||||
#: views.py:727
|
||||
msgid "User characteristics non allowed"
|
||||
msgstr "Caractéristique utilisateur non autorisée"
|
||||
|
||||
#: views.py:529
|
||||
#: views.py:734
|
||||
#, 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"
|
||||
|
||||
#: views.py:599
|
||||
#: views.py:824
|
||||
#, python-format
|
||||
msgid "Authentication renewal required by 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
|
||||
msgid "Authentication required by service %(name)s (%(url)s)."
|
||||
msgstr "Authentification requise par le service %(name)s (%(url)s)."
|
||||
|
||||
#: views.py:613
|
||||
#: views.py:838
|
||||
#, python-format
|
||||
msgid "Service %s non allowed"
|
||||
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 ""
|
||||
#~ "Error during service logout %(service)s:\n"
|
||||
#~ "%(error)s"
|
||||
|
|
|
@ -23,3 +23,4 @@ class Command(BaseCommand):
|
|||
|
||||
def handle(self, *args, **options):
|
||||
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 import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.core.mail import send_mail
|
||||
|
||||
import re
|
||||
import sys
|
||||
import smtplib
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from requests_futures.sessions import FuturesSession
|
||||
|
||||
import cas_server.utils as utils
|
||||
from . import VERSION
|
||||
|
||||
#: logger facility
|
||||
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 '\\'."
|
||||
)
|
||||
)
|
||||
#: 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(
|
||||
max_length=255,
|
||||
default="",
|
||||
blank=True,
|
||||
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`.
|
||||
restrict_users = models.BooleanField(
|
||||
|
@ -621,7 +624,7 @@ class ReplaceAttributName(models.Model):
|
|||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"name"),
|
||||
help_text=_(u"name of an attribut to send to the service, use * for all attributes")
|
||||
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`
|
||||
#: is used.
|
||||
|
@ -629,7 +632,7 @@ class ReplaceAttributName(models.Model):
|
|||
max_length=255,
|
||||
blank=True,
|
||||
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")
|
||||
)
|
||||
#: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributName` instances for a
|
||||
|
@ -656,8 +659,8 @@ class FilterAttributValue(models.Model):
|
|||
#: The name of a user attribute
|
||||
attribut = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"attribut"),
|
||||
help_text=_(u"Name of the attribut which must verify pattern")
|
||||
verbose_name=_(u"attribute"),
|
||||
help_text=_(u"Name of the attribute which must verify pattern")
|
||||
)
|
||||
#: A regular expression the attribute :attr:`attribut` value must verify. If :attr:`attribut`
|
||||
#: if a list, only one of the list values needs to match.
|
||||
|
@ -686,8 +689,8 @@ class ReplaceAttributValue(models.Model):
|
|||
#: Name the attribute: a key of :attr:`User.attributs`
|
||||
attribut = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_(u"attribut"),
|
||||
help_text=_(u"Name of the attribut for which the value must be replace")
|
||||
verbose_name=_(u"attribute"),
|
||||
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
|
||||
pattern = models.CharField(
|
||||
|
@ -1003,3 +1006,60 @@ class Proxy(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
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.respond}}"></script>
|
||||
<![endif]-->
|
||||
<link rel="shortcut icon" href="{% static "cas_server/favicon.ico?v=1" %}" />
|
||||
<link href="{% static "cas_server/login.css" %}" rel="stylesheet">
|
||||
{% if settings.CAS_FAVICON_URL %}<link rel="shortcut icon" href="{{settings.CAS_FAVICON_URL}}" />{% endif %}
|
||||
<link href="{% static "cas_server/styles.css" %}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
@ -31,23 +31,28 @@
|
|||
<div class="row">
|
||||
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
|
||||
{% block ante_messages %}{% endblock %}
|
||||
{% if auto_submit %}<noscript>{% endif %}
|
||||
{% 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 %}
|
||||
<div {% spaceless %}
|
||||
{% if message.level == message_levels.DEBUG %}
|
||||
class="alert alert-warning alert-dismissable"
|
||||
class="alert alert-warning"
|
||||
{% elif message.level == message_levels.INFO %}
|
||||
class="alert alert-info alert-dismissable"
|
||||
class="alert alert-info"
|
||||
{% elif message.level == message_levels.SUCCESS %}
|
||||
class="alert alert-success alert-dismissable"
|
||||
class="alert alert-success"
|
||||
{% elif message.level == message_levels.WARNING %}
|
||||
class="alert alert-warning alert-dismissable"
|
||||
class="alert alert-warning"
|
||||
{% else %}
|
||||
class="alert alert-danger alert-dismissable"
|
||||
class="alert alert-danger"
|
||||
{% endif %}
|
||||
{% endspaceless %}>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -59,5 +64,9 @@
|
|||
</div> <!-- /container -->
|
||||
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></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>
|
||||
</html>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
{% load cas_server %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{{error}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for field in form %}{% if field.display %}
|
||||
{% for field in form %}{% if not field|is_hidden %}
|
||||
<div class="form-group{% spaceless %}
|
||||
{% if not form.non_field_errors %}
|
||||
{% if field.errors %} has-error
|
||||
|
@ -12,7 +13,7 @@
|
|||
{% endif %}
|
||||
{% endif %}"
|
||||
{% endspaceless %}>{% spaceless %}
|
||||
{% if field.checkbox %}
|
||||
{% if field|is_checkbox %}
|
||||
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label>
|
||||
{% else %}
|
||||
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
{% extends "cas_server/base.html" %}
|
||||
{% load i18n %}
|
||||
{% 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">
|
||||
<div class="checkbox">
|
||||
<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>
|
||||
</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>
|
||||
</form>
|
||||
{% 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',
|
||||
]
|
||||
|
||||
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'
|
||||
|
||||
# Database
|
||||
|
@ -81,3 +97,30 @@ USE_TZ = True
|
|||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||
|
||||
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
|
||||
if remember:
|
||||
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
|
||||
response = client.post('/federate', params)
|
||||
# we are redirected to the provider CAS client url
|
||||
self.assertEqual(response.status_code, 302)
|
||||
if remember:
|
||||
self.assertEqual(response["Location"], '%s/federate/%s?remember=on' % (
|
||||
'http://testserver' if django.VERSION < (1, 9) else "",
|
||||
provider.suffix
|
||||
))
|
||||
else:
|
||||
self.assertEqual(response["Location"], '%s/federate/%s' % (
|
||||
'http://testserver' if django.VERSION < (1, 9) else "",
|
||||
provider.suffix
|
||||
))
|
||||
self.assertEqual(response["Location"], '%s/federate/%s%s' % (
|
||||
'http://testserver' if django.VERSION < (1, 9) else "",
|
||||
provider.suffix,
|
||||
"?remember=on" if remember else ""
|
||||
))
|
||||
# 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
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(
|
||||
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.suffix
|
||||
provider.suffix,
|
||||
"%3Fremember%3Don" if remember else ""
|
||||
)
|
||||
)
|
||||
# let's generate a ticket
|
||||
|
@ -114,7 +116,10 @@ class FederateAuthLoginLogoutTestCase(
|
|||
# we lauch a dummy CAS server that only validate once for the service
|
||||
# http://testserver/federate/example.com with `ticket`
|
||||
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"),
|
||||
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
|
||||
# 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["Location"], "%s/login" % (
|
||||
'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,
|
||||
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"
|
||||
bad_provider = "exemple.fr"
|
||||
|
@ -229,6 +241,18 @@ class FederateAuthLoginLogoutTestCase(
|
|||
'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):
|
||||
"""test that SLO receive from backend CAS log out the users"""
|
||||
# get tickets and connected clients
|
||||
|
@ -331,6 +355,76 @@ class FederateAuthLoginLogoutTestCase(
|
|||
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):
|
||||
"""
|
||||
Try login with a bad ticket:
|
||||
|
|
|
@ -16,7 +16,9 @@ import django
|
|||
from django.test import TestCase, Client
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import timezone
|
||||
from django.core import mail
|
||||
|
||||
import mock
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
|
@ -60,6 +62,25 @@ class FederatedUserTestCase(TestCase, UserModels, FederatedIendityProviderModel)
|
|||
with self.assertRaises(models.FederatedUser.DoesNotExist):
|
||||
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):
|
||||
"""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'])
|
||||
# only 1 ticket remain in the db
|
||||
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
|
||||
"""Tests module for utils"""
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.db import connection
|
||||
|
||||
import six
|
||||
import warnings
|
||||
import datetime
|
||||
|
||||
from cas_server import utils
|
||||
|
||||
|
@ -128,16 +131,23 @@ class CheckPasswordCase(TestCase):
|
|||
with self.assertRaises(utils.LdapHashUserPassword.BadHash):
|
||||
utils.check_password("ldap", self.password1, b"TOTOssdsdsd", "utf8")
|
||||
for scheme in schemes_salt:
|
||||
# bad length
|
||||
with self.assertRaises(utils.LdapHashUserPassword.BadHash):
|
||||
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):
|
||||
"""test all the hex_HASH method: the hashed password is a simple hash of the password"""
|
||||
hashes = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]
|
||||
hashed_password1 = []
|
||||
for hash in hashes:
|
||||
for hash_scheme in hashes:
|
||||
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:
|
||||
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, 'toto'), 'toto')
|
||||
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 json
|
||||
import mock
|
||||
from lxml import etree
|
||||
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
|
||||
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):
|
||||
"""Test a successul login"""
|
||||
# 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})
|
||||
# the ticket is not created and a warning is displayed to the user
|
||||
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
|
||||
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})
|
||||
# the ticket is not created and a warning is displayed to the user
|
||||
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
|
||||
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})
|
||||
# the ticket is not created and a warning is displayed to the user
|
||||
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):
|
||||
"""test gateway parameter"""
|
||||
|
|
|
@ -12,8 +12,9 @@
|
|||
"""Some utils functions for tests"""
|
||||
from cas_server.default_settings import settings
|
||||
|
||||
import django
|
||||
from django.test import Client
|
||||
from django.template import loader, Context
|
||||
from django.template import loader
|
||||
from django.utils import timezone
|
||||
|
||||
import cgi
|
||||
|
@ -21,13 +22,25 @@ import six
|
|||
from threading import Thread
|
||||
from lxml import etree
|
||||
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 cas_server import models
|
||||
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):
|
||||
"""make `string` a unicode if `string` is a unicode or bytes encoded with `charset`"""
|
||||
if not isinstance(string, six.text_type):
|
||||
|
@ -166,7 +179,7 @@ class HttpParamsHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
postvars = cgi.parse_multipart(self.rfile, pdict)
|
||||
elif ctype == 'application/x-www-form-urlencoded':
|
||||
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:
|
||||
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
|
||||
|
||||
app_name = "cas_server"
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', RedirectView.as_view(pattern_name="cas_server:login")),
|
||||
url(r'^$', RedirectView.as_view(pattern_name="cas_server:login", permanent=False)),
|
||||
url(
|
||||
'^login$',
|
||||
sensitive_post_parameters('password')(
|
||||
|
@ -51,8 +53,8 @@ urlpatterns = [
|
|||
url('^samlValidate$', views.SamlValidate.as_view(), name='samlValidate'),
|
||||
url(
|
||||
'^auth$',
|
||||
sensitive_variables('password')(
|
||||
sensitive_post_parameters('password')(
|
||||
sensitive_variables('password', 'secret')(
|
||||
sensitive_post_parameters('password', 'secret')(
|
||||
views.Auth.as_view()
|
||||
)
|
||||
),
|
||||
|
|
|
@ -25,11 +25,20 @@ import hashlib
|
|||
import crypt
|
||||
import base64
|
||||
import six
|
||||
import requests
|
||||
import time
|
||||
import logging
|
||||
import binascii
|
||||
|
||||
from importlib import import_module
|
||||
from datetime import datetime, timedelta
|
||||
from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
|
||||
|
||||
from . import VERSION
|
||||
|
||||
#: logger facility
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def json_encode(obj):
|
||||
"""Encode a python object to json"""
|
||||
|
@ -51,6 +60,14 @@ def context(params):
|
|||
"""
|
||||
params["settings"] = settings
|
||||
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
|
||||
|
||||
|
||||
|
@ -545,7 +562,10 @@ class LdapHashUserPassword(object):
|
|||
elif scheme == b'{CRYPT}':
|
||||
return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):]
|
||||
else:
|
||||
hashed_passord = base64.b64decode(hashed_passord[len(scheme):])
|
||||
try:
|
||||
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]:
|
||||
raise cls.BadHash("Hash too short for the scheme %s" % 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
|
||||
:type hashed_password: :obj:`str` or :obj:`unicode`
|
||||
:param str charset: The used char encoding (also used internally, so it must be valid for
|
||||
the charset used by ``password`` even if it is inputed as an :obj:`unicode`)
|
||||
the charset used by ``password`` when it was initially )
|
||||
:return: True if ``password`` match ``hashed_password`` using ``method``,
|
||||
``False`` otherwise
|
||||
:rtype: bool
|
||||
|
@ -603,3 +623,60 @@ def check_password(method, password, hashed_password, charset):
|
|||
)(password).hexdigest().encode("ascii") == hashed_password.lower()
|
||||
else:
|
||||
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
|
||||
if settings.CAS_FEDERATE:
|
||||
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()
|
||||
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 self.service:
|
||||
list(messages.get_messages(request)) # clean messages before leaving the django app
|
||||
|
@ -209,6 +212,7 @@ class LogoutView(View, LogoutMixin):
|
|||
|
||||
class FederateAuth(View):
|
||||
"""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
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
|
@ -218,8 +222,7 @@ class FederateAuth(View):
|
|||
"""
|
||||
return super(FederateAuth, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_cas_client(request, provider):
|
||||
def get_cas_client(self, request, provider, renew=False):
|
||||
"""
|
||||
return a CAS client object matching provider
|
||||
|
||||
|
@ -231,7 +234,8 @@ class FederateAuth(View):
|
|||
"""
|
||||
# compute the current url, ignoring ticket dans provider GET parameters
|
||||
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):
|
||||
"""
|
||||
|
@ -264,24 +268,16 @@ class FederateAuth(View):
|
|||
if form.is_valid():
|
||||
params = utils.copy_params(
|
||||
request.POST,
|
||||
ignore={"provider", "csrfmiddlewaretoken", "ticket"}
|
||||
ignore={"provider", "csrfmiddlewaretoken", "ticket", "lt"}
|
||||
)
|
||||
if params.get("renew") == "False":
|
||||
del params["renew"]
|
||||
url = utils.reverse_params(
|
||||
"cas_server:federateAuth",
|
||||
kwargs=dict(provider=form.cleaned_data["provider"].suffix),
|
||||
params=params
|
||||
)
|
||||
response = 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
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return redirect("cas_server:login")
|
||||
|
||||
|
@ -296,47 +292,81 @@ class FederateAuth(View):
|
|||
if not settings.CAS_FEDERATE:
|
||||
logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode")
|
||||
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
|
||||
# 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")
|
||||
return redirect("cas_server:login")
|
||||
try:
|
||||
# get the identity provider from its suffix
|
||||
provider = FederatedIendityProvider.objects.get(suffix=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 'ticket' not in request.GET:
|
||||
logger.info("Trying to authenticate again %s" % auth.provider.server_url)
|
||||
return HttpResponseRedirect(auth.get_login_url())
|
||||
else:
|
||||
ticket = request.GET['ticket']
|
||||
# if the ticket validation succeed
|
||||
if auth.verify_ticket(ticket):
|
||||
logger.info(
|
||||
"Got a valid ticket for %s from %s" % (
|
||||
auth.username,
|
||||
auth.provider.server_url
|
||||
try:
|
||||
# if the ticket validation succeed
|
||||
if auth.verify_ticket(ticket):
|
||||
logger.info(
|
||||
"Got a valid ticket for %s from %s" % (
|
||||
auth.username,
|
||||
auth.provider.server_url
|
||||
)
|
||||
)
|
||||
)
|
||||
params = utils.copy_params(request.GET, ignore={"ticket"})
|
||||
request.session["federate_username"] = auth.federated_username
|
||||
request.session["federate_ticket"] = ticket
|
||||
auth.register_slo(auth.federated_username, request.session.session_key, ticket)
|
||||
# redirect to the the login page for the user to become authenticated
|
||||
# thanks to the `federate_username` and `federate_ticket` session parameters
|
||||
url = utils.reverse_params("cas_server:login", params)
|
||||
return HttpResponseRedirect(url)
|
||||
# else redirect to the identity provider CAS login page
|
||||
else:
|
||||
logger.info(
|
||||
"Got a invalid ticket for %s from %s. Retrying to authenticate" % (
|
||||
auth.username,
|
||||
auth.provider.server_url
|
||||
params = utils.copy_params(request.GET, ignore={"ticket", "remember"})
|
||||
request.session["federate_username"] = auth.federated_username
|
||||
request.session["federate_ticket"] = ticket
|
||||
auth.register_slo(
|
||||
auth.federated_username,
|
||||
request.session.session_key,
|
||||
ticket
|
||||
)
|
||||
# redirect to the the login page for the user to become authenticated
|
||||
# thanks to the `federate_username` and `federate_ticket` session parameters
|
||||
url = utils.reverse_params("cas_server:login", params)
|
||||
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:
|
||||
logger.info(
|
||||
(
|
||||
"Got a invalid ticket %s from %s for service %s. "
|
||||
"Retrying to authenticate"
|
||||
) % (
|
||||
ticket,
|
||||
auth.provider.server_url,
|
||||
self.service_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}
|
||||
)
|
||||
return HttpResponseRedirect(auth.get_login_url())
|
||||
response = redirect("cas_server:login")
|
||||
response.delete_cookie("remember_provider")
|
||||
return response
|
||||
except FederatedIendityProvider.DoesNotExist:
|
||||
logger.warning("Identity provider suffix %s not found" % provider)
|
||||
# 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')
|
||||
if settings.CAS_FEDERATE:
|
||||
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):
|
||||
"""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(
|
||||
self.request,
|
||||
messages.ERROR,
|
||||
_(u"Invalid login ticket")
|
||||
_(u"Invalid login ticket, please retry to login")
|
||||
)
|
||||
elif ret == self.USER_LOGIN_OK:
|
||||
# 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)
|
||||
raise EnvironmentError("invalid output for LoginView.process_post")
|
||||
# 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):
|
||||
"""
|
||||
|
@ -586,7 +627,9 @@ class LoginView(View, LogoutMixin):
|
|||
form_initial = {
|
||||
'service': self.service,
|
||||
'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],
|
||||
'renew': self.renew
|
||||
}
|
||||
|
@ -683,14 +726,14 @@ class LoginView(View, LogoutMixin):
|
|||
messages.add_message(
|
||||
self.request,
|
||||
messages.ERROR,
|
||||
_(u"User charateristics non allowed")
|
||||
_(u"User characteristics non allowed")
|
||||
)
|
||||
except models.UserFieldNotDefined:
|
||||
error = 4
|
||||
messages.add_message(
|
||||
self.request,
|
||||
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}
|
||||
)
|
||||
|
||||
|
@ -817,19 +860,37 @@ class LoginView(View, LogoutMixin):
|
|||
)
|
||||
else:
|
||||
if (
|
||||
self.request.COOKIES.get('_remember_provider') and
|
||||
self.request.COOKIES.get('remember_provider') and
|
||||
FederatedIendityProvider.objects.filter(
|
||||
suffix=self.request.COOKIES['_remember_provider']
|
||||
suffix=self.request.COOKIES['remember_provider']
|
||||
)
|
||||
):
|
||||
params = utils.copy_params(self.request.GET)
|
||||
url = utils.reverse_params(
|
||||
"cas_server:federateAuth",
|
||||
params=params,
|
||||
kwargs=dict(provider=self.request.COOKIES['_remember_provider'])
|
||||
kwargs=dict(provider=self.request.COOKIES['remember_provider'])
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
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(
|
||||
self.request,
|
||||
settings.CAS_LOGIN_TEMPLATE,
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
setuptools>=5.5
|
||||
requests>=2.4
|
||||
requests_futures>=0.9.5
|
||||
lxml>=3.4
|
||||
six>=1.8
|
||||
tox>=1.8.1
|
||||
pytest>=2.6.4
|
||||
pytest-django>=2.8.0
|
||||
pytest-pythonpath>=0.3
|
||||
pytest-warnings
|
||||
pytest-cov>=2.2.1
|
||||
requests>=2.4
|
||||
requests_futures>=0.9.5
|
||||
lxml>=3.4
|
||||
six>=1
|
||||
mock>=1
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Django >= 1.8,<1.10
|
||||
Django >= 1.7.1,<1.10
|
||||
setuptools>=5.5
|
||||
requests>=2.4
|
||||
requests_futures>=0.9.5
|
||||
lxml>=3.4
|
||||
six>=1
|
||||
six>=1.8
|
||||
|
|
7
setup.py
7
setup.py
|
@ -1,8 +1,7 @@
|
|||
import os
|
||||
import pkg_resources
|
||||
from setuptools import setup
|
||||
|
||||
VERSION = '0.6.1'
|
||||
from cas_server import VERSION
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
|
||||
README = readme.read()
|
||||
|
@ -30,7 +29,7 @@ if __name__ == '__main__':
|
|||
author_email='valentin.samir@crans.org',
|
||||
classifiers=[
|
||||
'Environment :: Web Environment',
|
||||
'evelopment Status :: 5 - Production/Stable',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.7',
|
||||
'Framework :: Django :: 1.8',
|
||||
|
@ -66,5 +65,5 @@ if __name__ == '__main__':
|
|||
download_url="https://github.com/nitmir/django-cas-server/releases",
|
||||
zip_safe=False,
|
||||
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]
|
||||
commands=
|
||||
py.test {posargs:cas_server/tests/}
|
||||
py.test -rw {posargs:cas_server/tests/}
|
||||
{[post_cmd]commands}
|
||||
whitelist_externals={[post_cmd]whitelist_externals}
|
||||
|
||||
|
|
Loading…
Reference in a new issue