Merge pull request #9 from nitmir/dev

Update to version 0.6.2
This commit is contained in:
Valentin Samir 2016-08-02 14:44:30 +02:00 committed by GitHub
commit ba59060763
36 changed files with 1255 additions and 699 deletions

View File

@ -1,8 +1,6 @@
language: python language: python
matrix: matrix:
include: include:
- python: "2.7"
env: TOX_ENV=coverage
- python: "2.7" - python: "2.7"
env: TOX_ENV=flake8 env: TOX_ENV=flake8
- python: "2.7" - python: "2.7"
@ -23,6 +21,8 @@ matrix:
env: TOX_ENV=py35-django18 env: TOX_ENV=py35-django18
- python: "3.5" - python: "3.5"
env: TOX_ENV=py35-django19 env: TOX_ENV=py35-django19
- python: "2.7"
env: TOX_ENV=coverage
cache: cache:
directories: directories:
- $HOME/.cache/pip/http/ - $HOME/.cache/pip/http/

View File

@ -38,7 +38,7 @@ dist:
test_venv/bin/python: test_venv/bin/python:
virtualenv test_venv virtualenv test_venv
test_venv/bin/pip install -U --requirement requirements-dev.txt Django test_venv/bin/pip install -U --requirement requirements-dev.txt 'Django<1.10'
test_venv/cas/manage.py: test_venv test_venv/cas/manage.py: test_venv
mkdir -p test_venv/cas mkdir -p test_venv/cas
@ -62,7 +62,7 @@ run_server: test_project
run_tests: test_venv run_tests: test_venv
python setup.py check --restructuredtext --stric python setup.py check --restructuredtext --stric
test_venv/bin/py.test --cov=cas_server --cov-report html test_venv/bin/py.test -rw -x --cov=cas_server --cov-report html
rm htmlcov/coverage_html.js # I am really pissed off by those keybord shortcuts rm htmlcov/coverage_html.js # I am really pissed off by those keybord shortcuts
test_venv/bin/sphinx-build: test_venv test_venv/bin/sphinx-build: test_venv

View File

@ -29,11 +29,49 @@ Dependencies
``django-cas-server`` depends on the following python packages: ``django-cas-server`` depends on the following python packages:
* Django >= 1.7 < 1.10 * Django >= 1.7.1 < 1.10
* requests >= 2.4 * requests >= 2.4
* requests_futures >= 0.9.5 * requests_futures >= 0.9.5
* lxml >= 3.4 * lxml >= 3.4
* six >= 1 * six >= 1.8
Minimal version of packages dependancy are just indicative and meens that ``django-cas-server`` has
been tested with it. Previous versions of dependencies may or may not work.
Additionally, denpending of the authentication backend you plan to use, you may need the following
python packages:
* ldap3
* psycopg2
* mysql-python
Here there is a table with the name of python packages and the corresponding packages providing
them on debian like systems and centos like systems.
You should try as much as possible to use system packages as there are automatically updated then
you update your system. You can then install Not Available (N/A)
packages on your system using pip inside a virtualenv as described in the `Installation`_ section.
For use with python3, just replace python(2) in the table by python3.
+------------------+-------------------------+---------------------+
| python package | debian like systems | centos like systems |
+==================+=========================+=====================+
| Django | python-django | python-django |
+------------------+-------------------------+---------------------+
| requests | python-requests | python-requests |
+------------------+-------------------------+---------------------+
| requests_futures | python-requests-futures | N/A |
+------------------+-------------------------+---------------------+
| lxml | python-lxml | python-lxml |
+------------------+-------------------------+---------------------+
| six | python-six | python-six |
+------------------+-------------------------+---------------------+
| ldap3 | python-ldap3 | python-ldap3 |
+------------------+-------------------------+---------------------+
| psycopg2 | python-psycopg2 | python-psycopg2 |
+------------------+-------------------------+---------------------+
| mysql-python | python-mysqldb | python2-mysql |
+------------------+-------------------------+---------------------+
Installation Installation
============ ============
@ -63,14 +101,17 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
New python executable in cas/bin/python2 New python executable in cas/bin/python2
Also creating executable in cas/bin/python Also creating executable in cas/bin/python
Installing setuptools, pip...done. Installing setuptools, pip...done.
4. And `activate it <https://virtualenv.pypa.io/en/stable/userguide/#activate-script>`__::
$ cd cas_venv/; . bin/activate $ cd cas_venv/; . bin/activate
4. Create a django project:: 5. Create a django project::
$ django-admin startproject cas_project $ django-admin startproject cas_project
$ cd cas_project $ cd cas_project
5. Install `django-cas-server`. To use the last published release, run:: 6. Install `django-cas-server`. To use the last published release, run::
$ pip install django-cas-server $ pip install django-cas-server
@ -81,11 +122,11 @@ The recommended installation mode is to use a virtualenv with ``--system-site-pa
$ pip install -r requirements.txt $ pip install -r requirements.txt
Then, either run ``make install`` to create a python package using the sources of the repository Then, either run ``make install`` to create a python package using the sources of the repository
and install it with pip, or place the `cas_server` directory into your and install it with pip, or place the ``cas_server`` directory into your
`PYTHONPATH <https://docs.python.org/2/using/cmdline.html#envvar-PYTHONPATH>`_ `PYTHONPATH <https://docs.python.org/2/using/cmdline.html#envvar-PYTHONPATH>`_
(for instance by symlinking `cas_server` to the root of your django project). (for instance by symlinking ``cas_server`` to the root of your django project).
6. Open ``cas_project/settings.py`` in you favourite editor and follow the quick start section. 7. Open ``cas_project/settings.py`` in you favourite editor and follow the quick start section.
Quick start Quick start
@ -145,7 +186,7 @@ Quick start
6. Start the development server and visit http://127.0.0.1:8000/admin/ 6. Start the development server and visit http://127.0.0.1:8000/admin/
to add a first service allowed to authenticate user against the CAS to add a first service allowed to authenticate user against the CAS
(you'll need the Admin app enabled). See the Service Patterns section bellow. (you'll need the Admin app enabled). See the `Service Patterns`_ section bellow.
7. Visit http://127.0.0.1:8000/cas/ to login with your django users. 7. Visit http://127.0.0.1:8000/cas/ to login with your django users.
@ -163,6 +204,8 @@ Template settings
* ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default * ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default
templates. Set it to ``False`` to disable it. templates. Set it to ``False`` to disable it.
* ``CAS_FAVICON_URL``: URL to the favicon (shortcut icon) used by the default templates.
Default is a key icon. Set it to ``False`` to disable it.
* ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary * ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary
and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``, and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is:: ``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
@ -193,6 +236,8 @@ Authentication settings
* ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing * ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing
``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"`` ``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"``
Available classes bundled with ``django-cas-server`` are listed below in the
`Authentication backend`_ section.
* ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after * ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
@ -212,13 +257,23 @@ Authentication settings
Federation settings Federation settings
------------------- -------------------
* ``CAS_FEDERATE``: A boolean for activating the federated mode (see the federate section below). * ``CAS_FEDERATE``: A boolean for activating the federated mode (see the `Federation mode`_
The default is ``False``. section below). The default is ``False``.
* ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity * ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity
provider" expire. The default is ``604800``, one week. The cookie is called provider" expire. The default is ``604800``, one week. The cookie is called
``_remember_provider``. ``_remember_provider``.
New version warnings settings
-----------------------------
* ``CAS_NEW_VERSION_HTML_WARNING``: A boolean for diplaying a warning on html pages then a new
version of the application is avaible. Once closed by a user, it is not displayed to this user
until the next new version. The default is ``True``.
* ``CAS_NEW_VERSION_EMAIL_WARNING``: A bolean sot sending a email to ``settings.ADMINS`` when a new
version is available. The default is ``True``.
Tickets validity settings Tickets validity settings
------------------------- -------------------------
@ -257,6 +312,7 @@ Tickets miscellaneous settings
Mysql backend settings Mysql backend settings
---------------------- ----------------------
Deprecated, see the `Sql backend settings`_.
Only usefull if you are using the mysql authentication backend: Only usefull if you are using the mysql authentication backend:
* ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``. * ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``.
@ -283,6 +339,64 @@ Only usefull if you are using the mysql authentication backend:
The default is ``"crypt"``. The default is ``"crypt"``.
Sql backend settings
--------------------
Only usefull if you are using the sql authentication backend. You must add a ``"cas_server"``
database to `settings.DATABASES <https://docs.djangoproject.com/fr/1.9/ref/settings/#std:setting-DATABASES>`__
as defined in the django documentation. It is then the database
use by the sql backend.
* ``CAS_SQL_USER_QUERY``: The query performed upon user authentication.
The username must be in field ``username``, the password in ``password``,
additional fields are used as the user attributes.
The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"``
* ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following:
* ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database
should begin this $
* ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html)
the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256},
{SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}.
* ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512.
The hashed password in the database is compare to the hexadecimal digest of the clear
password hashed with the corresponding algorithm.
* ``"plain"``, the password in the database must be in clear.
The default is ``"crypt"``.
* ``CAS_SQL_PASSWORD_CHARSET``: Charset the SQL users passwords was hash with. This is needed to
encode the user sended password before hashing it for comparison. The default is ``"utf-8"``.
Ldap backend settings
---------------------
Only usefull if you are using the ldap authentication backend:
* ``CAS_LDAP_SERVER``: Address of the LDAP server. The default is ``"localhost"``.
* ``CAS_LDAP_USER``: User bind address, for example ``"cn=admin,dc=crans,dc=org"`` for
connecting to the LDAP server.
* ``CAS_LDAP_PASSWORD``: Password for connecting to the LDAP server.
* ``CAS_LDAP_BASE_DN``: LDAP search base DN, for example ``"ou=data,dc=crans,dc=org"``.
* ``CAS_LDAP_USER_QUERY``: Search filter for searching user by username. User inputed usernames are
escaped using ``ldap3.utils.conv.escape_bytes``. The default is ``"(uid=%s)"``
* ``CAS_LDAP_USERNAME_ATTR``: Attribute used for users usernames. The default is ``"uid"``
* ``CAS_LDAP_PASSWORD_ATTR``: Attribute used for users passwords. The default is ``"userPassword"``
* ``CAS_LDAP_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following:
* ``"crypt"`` (see <https://en.wikipedia.org/wiki/Crypt_(C)>), the password in the database
should begin this $
* ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html)
the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256},
{SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}.
* ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512.
The hashed password in the database is compare to the hexadecimal digest of the clear
password hashed with the corresponding algorithm.
* ``"plain"``, the password in the database must be in clear.
The default is ``"ldap"``.
* ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to
encode the user sended password before hashing it for comparison. The default is ``"utf-8"``.
Test backend settings Test backend settings
--------------------- ---------------------
Only usefull if you are using the test authentication backend: Only usefull if you are using the test authentication backend:
@ -304,11 +418,17 @@ Authentication backend
for the user are defined by the ``CAS_TEST_*`` settings. for the user are defined by the ``CAS_TEST_*`` settings.
* django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system. * django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system.
This is the default backend. The returned attributes are the fields available on the user model. This is the default backend. The returned attributes are the fields available on the user model.
* mysql backend ``cas_server.auth.MysqlAuthUser``: see the 'Mysql backend settings' section. * mysql backend ``cas_server.auth.MysqlAuthUser``: Deprecated, use the sql backend instead.
see the `Mysql backend settings`_ section. The returned attributes are those return by sql query
``CAS_SQL_USER_QUERY``.
* sql backend ``cas_server.auth.SqlAuthUser``: see the `Sql backend settings`_ section.
The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``. The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``.
* ldap backend ``cas_server.auth.LdapAuthUser``: see the `Ldap backend settings`_ section.
The returned attributes are those of the ldap node returned by the query filter ``CAS_LDAP_USER_QUERY``.
* federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``. * federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``.
You should not set it manually without setting ``CAS_FEDERATE`` to ``True``. You should not set it manually without setting ``CAS_FEDERATE`` to ``True``.
Logs Logs
==== ====

View File

@ -9,5 +9,9 @@
# #
# (c) 2015-2016 Valentin Samir # (c) 2015-2016 Valentin Samir
"""A django CAS server application""" """A django CAS server application"""
#: version of the application
VERSION = '0.6.2'
#: path the the application configuration class #: path the the application configuration class
default_app_config = 'cas_server.apps.CasAppConfig' default_app_config = 'cas_server.apps.CasAppConfig'

View File

@ -13,16 +13,25 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils import timezone from django.utils import timezone
from django.db import connections, DatabaseError
import warnings
from datetime import timedelta from datetime import timedelta
from six.moves import range
try: # pragma: no cover try: # pragma: no cover
import MySQLdb import MySQLdb
import MySQLdb.cursors import MySQLdb.cursors
from utils import check_password
except ImportError: except ImportError:
MySQLdb = None MySQLdb = None
try: # pragma: no cover
import ldap3
except ImportError:
ldap3 = None
from .models import FederatedUser from .models import FederatedUser
from .utils import check_password, dictfetchall
class AuthUser(object): class AuthUser(object):
@ -116,19 +125,46 @@ class TestAuthUser(AuthUser):
return {} return {}
class MysqlAuthUser(AuthUser): # pragma: no cover class DBAuthUser(AuthUser): # pragma: no cover
"""base class for databate based auth classes"""
#: DB user attributes as a :class:`dict` if the username is found in the database.
user = None
def attributs(self):
""" """
A mysql authentication class: authentication user agains a mysql database The user attributes.
:return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode`
or :class:`list` of :func:`unicode`. If the user do not exists, the returned
:class:`dict` is empty.
:rtype: dict
"""
if self.user:
return self.user
else:
return {}
class MysqlAuthUser(DBAuthUser): # pragma: no cover
"""
DEPRECATED, use :class:`SqlAuthUser` instead.
A mysql authentication class: authenticate user agains a mysql database
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>` :param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are fetched from the MySQL database set with class attribute. Valid value are fetched from the MySQL database set with
``settings.CAS_SQL_*`` settings parameters using the query ``settings.CAS_SQL_*`` settings parameters using the query
``settings.CAS_SQL_USER_QUERY``. ``settings.CAS_SQL_USER_QUERY``.
""" """
#: Mysql user attributes as a :class:`dict` if the username is found in the database.
user = None
def __init__(self, username): def __init__(self, username):
warnings.warn(
(
"MysqlAuthUser authentication class is deprecated: "
"use cas_server.auth.SqlAuthUser instead"
),
UserWarning
)
# see the connect function at # see the connect function at
# http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes # http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes
# for possible mysql config parameters. # for possible mysql config parameters.
@ -169,24 +205,130 @@ class MysqlAuthUser(AuthUser): # pragma: no cover
else: else:
return False return False
def attributs(self):
"""
The user attributes.
:return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode` class SqlAuthUser(DBAuthUser): # pragma: no cover
or :class:`list` of :func:`unicode`. If the user do not exists, the returned """
:class:`dict` is empty. A SQL authentication class: authenticate user agains a SQL database. The SQL database
:rtype: dict must be configures in settings.py as ``settings.DATABASES['cas_server']``.
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are fetched from the MySQL database set with
``settings.CAS_SQL_*`` settings parameters using the query
``settings.CAS_SQL_USER_QUERY``.
"""
def __init__(self, username):
if "cas_server" not in connections:
raise RuntimeError("Please configure the 'cas_server' database in settings.DATABASES")
for retry_nb in range(3):
try:
with connections["cas_server"].cursor() as curs:
curs.execute(settings.CAS_SQL_USER_QUERY, (username,))
results = dictfetchall(curs)
if len(results) == 1:
self.user = results[0]
super(SqlAuthUser, self).__init__(self.user['username'])
else:
super(SqlAuthUser, self).__init__(username)
break
except DatabaseError:
connections["cas_server"].close()
if retry_nb == 2:
raise
def test_password(self, password):
"""
Tests ``password`` agains the user password.
:param unicode password: a clear text password as submited by the user.
:return: ``True`` if :attr:`username<AuthUser.username>` is valid and ``password`` is
correct, ``False`` otherwise.
:rtype: bool
""" """
if self.user: if self.user:
return self.user return check_password(
settings.CAS_SQL_PASSWORD_CHECK,
password,
self.user["password"],
settings.CAS_SQL_PASSWORD_CHARSET
)
else: else:
return {} return False
class LdapAuthUser(DBAuthUser): # pragma: no cover
"""
A ldap authentication class: authenticate user against a ldap database
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are fetched from the ldap database set with
``settings.CAS_LDAP_*`` settings parameters.
"""
_conn = None
@classmethod
def get_conn(cls):
"""Return a connection object to the ldap database"""
conn = cls._conn
if conn is None or conn.closed:
conn = ldap3.Connection(
settings.CAS_LDAP_SERVER,
settings.CAS_LDAP_USER,
settings.CAS_LDAP_PASSWORD,
auto_bind=True
)
cls._conn = conn
return conn
def __init__(self, username):
if not ldap3:
raise RuntimeError("Please install ldap3 before using the LdapAuthUser backend")
# in case we got deconnected from the database, retry to connect 2 times
for retry_nb in range(3):
try:
conn = self.get_conn()
if conn.search(
settings.CAS_LDAP_BASE_DN,
settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(username),
attributes=ldap3.ALL_ATTRIBUTES
) and len(conn.entries) == 1:
user = conn.entries[0].entry_get_attributes_dict()
if user.get(settings.CAS_LDAP_USERNAME_ATTR):
self.user = user
super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0])
else:
super(LdapAuthUser, self).__init__(username)
else:
super(LdapAuthUser, self).__init__(username)
break
except ldap3.LDAPCommunicationError:
if retry_nb == 2:
raise
def test_password(self, password):
"""
Tests ``password`` agains the user password.
:param unicode password: a clear text password as submited by the user.
:return: ``True`` if :attr:`username<AuthUser.username>` is valid and ``password`` is
correct, ``False`` otherwise.
:rtype: bool
"""
if self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR):
return check_password(
settings.CAS_LDAP_PASSWORD_CHECK,
password,
self.user[settings.CAS_LDAP_PASSWORD_ATTR][0],
settings.CAS_LDAP_PASSWORD_CHARSET
)
else:
return False
class DjangoAuthUser(AuthUser): # pragma: no cover class DjangoAuthUser(AuthUser): # pragma: no cover
""" """
A django auth class: authenticate user agains django internal users A django auth class: authenticate user against django internal users
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>` :param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. Valid value are usernames of django internal users. class attribute. Valid value are usernames of django internal users.

View File

@ -134,6 +134,14 @@ class CASClientBase(object):
raise CASError(errors[0].attrib['code'], errors[0].text) raise CASError(errors[0].attrib['code'], errors[0].text)
raise CASError("Bad http code %s" % response.code) raise CASError("Bad http code %s" % response.code)
@staticmethod
def get_page_charset(page, default="utf-8"):
content_type = page.info().get('Content-type')
if content_type and "charset=" in content_type:
return content_type.split("charset=")[-1]
else:
return default
class CASClientV1(CASClientBase, ReturnUnicode): class CASClientV1(CASClientBase, ReturnUnicode):
"""CAS Client Version 1""" """CAS Client Version 1"""
@ -146,17 +154,15 @@ class CASClientV1(CASClientBase, ReturnUnicode):
Returns username on success and None on failure. Returns username on success and None on failure.
""" """
params = [('ticket', ticket), ('service', self.service_url)] params = [('ticket', ticket), ('service', self.service_url)]
if self.renew:
params.append(('renew', 'true'))
url = (urllib_parse.urljoin(self.server_url, 'validate') + '?' + url = (urllib_parse.urljoin(self.server_url, 'validate') + '?' +
urllib_parse.urlencode(params)) urllib_parse.urlencode(params))
page = urllib_request.urlopen(url) page = urllib_request.urlopen(url)
try: try:
verified = page.readline().strip() verified = page.readline().strip()
if verified == b'yes': if verified == b'yes':
content_type = page.info().get('Content-type') charset = self.get_page_charset(page, default="ascii")
if "charset=" in content_type:
charset = content_type.split("charset=")[-1]
else:
charset = "ascii"
user = self.u(page.readline().strip(), charset) user = self.u(page.readline().strip(), charset)
return user, None, None return user, None, None
else: else:
@ -183,17 +189,15 @@ class CASClientV2(CASClientBase, ReturnUnicode):
def get_verification_response(self, ticket): def get_verification_response(self, ticket):
params = [('ticket', ticket), ('service', self.service_url)] params = [('ticket', ticket), ('service', self.service_url)]
if self.renew:
params.append(('renew', 'true'))
if self.proxy_callback: if self.proxy_callback:
params.append(('pgtUrl', self.proxy_callback)) params.append(('pgtUrl', self.proxy_callback))
base_url = urllib_parse.urljoin(self.server_url, self.url_suffix) base_url = urllib_parse.urljoin(self.server_url, self.url_suffix)
url = base_url + '?' + urllib_parse.urlencode(params) url = base_url + '?' + urllib_parse.urlencode(params)
page = urllib_request.urlopen(url) page = urllib_request.urlopen(url)
try: try:
content_type = page.info().get('Content-type') charset = self.get_page_charset(page)
if "charset=" in content_type:
charset = content_type.split("charset=")[-1]
else:
charset = "ascii"
return (page.read(), charset) return (page.read(), charset)
finally: finally:
page.close() page.close()
@ -306,11 +310,7 @@ class CASClientWithSAMLV1(CASClientV2, SingleLogoutMixin):
from elementtree import ElementTree from elementtree import ElementTree
page = self.fetch_saml_validation(ticket) page = self.fetch_saml_validation(ticket)
content_type = page.info().get('Content-type') charset = self.get_page_charset(page)
if "charset=" in content_type:
charset = content_type.split("charset=")[-1]
else:
charset = "ascii"
try: try:
user = None user = None

View File

@ -18,6 +18,8 @@ from importlib import import_module
#: URL to the logo showed in the up left corner on the default templates. #: URL to the logo showed in the up left corner on the default templates.
CAS_LOGO_URL = static("cas_server/logo.png") CAS_LOGO_URL = static("cas_server/logo.png")
#: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon.
CAS_FAVICON_URL = static("cas_server/favicon.ico")
#: URLs to css and javascript external components. #: URLs to css and javascript external components.
CAS_COMPONENT_URLS = { CAS_COMPONENT_URLS = {
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", "bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
@ -110,12 +112,39 @@ CAS_SQL_PASSWORD = ''
CAS_SQL_DBNAME = '' CAS_SQL_DBNAME = ''
#: Database charset. #: Database charset.
CAS_SQL_DBCHARSET = 'utf8' CAS_SQL_DBCHARSET = 'utf8'
#: The query performed upon user authentication. #: The query performed upon user authentication.
CAS_SQL_USER_QUERY = 'SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s' CAS_SQL_USER_QUERY = 'SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s'
#: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``, #: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``,
#: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, #: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``,
#: ``"hex_sha512"``, ``"plain"``. #: ``"hex_sha512"``, ``"plain"``.
CAS_SQL_PASSWORD_CHECK = 'crypt' # crypt or plain CAS_SQL_PASSWORD_CHECK = 'crypt'
#: charset the SQL users passwords was hash with
CAS_SQL_PASSWORD_CHARSET = "utf-8"
#: Address of the LDAP server
CAS_LDAP_SERVER = 'localhost'
#: LDAP user bind address, for example ``"cn=admin,dc=crans,dc=org"`` for connecting to the LDAP
#: server.
CAS_LDAP_USER = None
#: LDAP connection password
CAS_LDAP_PASSWORD = None
#: LDAP seach base DN, for example ``"ou=data,dc=crans,dc=org"``.
CAS_LDAP_BASE_DN = None
#: LDAP search filter for searching user by username. User inputed usernames are escaped using
#: :func:`ldap3.utils.conv.escape_bytes`.
CAS_LDAP_USER_QUERY = "(uid=%s)"
#: LDAP attribute used for users usernames
CAS_LDAP_USERNAME_ATTR = "uid"
#: LDAP attribute used for users passwords
CAS_LDAP_PASSWORD_ATTR = "userPassword"
#: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``,
#: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``,
#: ``"hex_sha512"``, ``"plain"``.
CAS_LDAP_PASSWORD_CHECK = "ldap"
#: charset the LDAP users passwords was hash with
CAS_LDAP_PASSWORD_CHARSET = "utf-8"
#: Username of the test user. #: Username of the test user.
@ -140,6 +169,15 @@ CAS_FEDERATE = False
#: Time after witch the cookie use for “remember my identity provider” expire (one week). #: Time after witch the cookie use for “remember my identity provider” expire (one week).
CAS_FEDERATE_REMEMBER_TIMEOUT = 604800 CAS_FEDERATE_REMEMBER_TIMEOUT = 604800
#: A :class:`bool` for diplaying a warning on html pages then a new version of the application
#: is avaible. Once closed by a user, it is not displayed to this user until the next new version.
CAS_NEW_VERSION_HTML_WARNING = True
#: A :class:`bool` for sending emails to ``settings.ADMINS`` when a new version is available.
CAS_NEW_VERSION_EMAIL_WARNING = True
#: URL to the pypi json of the application. Used to retreive the version number of the last version.
#: You should not change it.
CAS_NEW_VERSION_JSON_URL = "https://pypi.python.org/pypi/django-cas-server/json"
GLOBALS = globals().copy() GLOBALS = globals().copy()
for name, default_value in GLOBALS.items(): for name, default_value in GLOBALS.items():
# get the current setting value, falling back to default_value # get the current setting value, falling back to default_value

View File

@ -42,13 +42,13 @@ class CASFederateValidateUser(object):
#: the identity provider #: the identity provider
provider = None provider = None
def __init__(self, provider, service_url): def __init__(self, provider, service_url, renew=False):
self.provider = provider self.provider = provider
self.client = CASClient( self.client = CASClient(
service_url=service_url, service_url=service_url,
version=provider.cas_protocol_version, version=provider.cas_protocol_version,
server_url=provider.server_url, server_url=provider.server_url,
renew=False, renew=renew,
) )
def get_login_url(self): def get_login_url(self):

View File

@ -19,49 +19,56 @@ import cas_server.models as models
class BootsrapForm(forms.Form): class BootsrapForm(forms.Form):
"""Form base class to use boostrap then rendering the form fields""" """
Bases: :class:`django.forms.Form`
Form base class to use boostrap then rendering the form fields
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BootsrapForm, self).__init__(*args, **kwargs) super(BootsrapForm, self).__init__(*args, **kwargs)
for (name, field) in self.fields.items(): for field in self.fields.values():
# Only tweak the fiel if it will be displayed # Only tweak the fiel if it will be displayed
if not isinstance(field.widget, forms.HiddenInput): if not isinstance(field.widget, forms.HiddenInput):
# tell to display the field (used in form.html)
self[name].display = True
attrs = {} attrs = {}
if isinstance(field.widget, forms.CheckboxInput): if not isinstance(field.widget, forms.CheckboxInput):
self[name].checkbox = True
else:
attrs['class'] = "form-control" attrs['class'] = "form-control"
if field.label: if field.label: # pragma: no branch (currently all field are hidden or labeled)
attrs["placeholder"] = field.label attrs["placeholder"] = field.label
if field.required: if field.required:
attrs["required"] = "required" attrs["required"] = "required"
field.widget.attrs.update(attrs) field.widget.attrs.update(attrs)
class WarnForm(BootsrapForm): class BaseLogin(BootsrapForm):
""" """
Bases: :class:`django.forms.Form` Bases: :class:`BootsrapForm`
Form used on warn page before emiting a ticket Base form with all field possibly hidden on the login pages
""" """
#: The service url for which the user want a ticket #: The service url for which the user want a ticket
service = forms.CharField(widget=forms.HiddenInput(), required=False) service = forms.CharField(widget=forms.HiddenInput(), required=False)
#: A valid LoginTicket to prevent POST replay
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
#: Is the service asking the authentication renewal ? #: Is the service asking the authentication renewal ?
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False) renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
#: Url to redirect to if the authentication fail (user not authenticated or bad service) #: Url to redirect to if the authentication fail (user not authenticated or bad service)
gateway = forms.CharField(widget=forms.HiddenInput(), required=False) gateway = forms.CharField(widget=forms.HiddenInput(), required=False)
method = forms.CharField(widget=forms.HiddenInput(), required=False) method = forms.CharField(widget=forms.HiddenInput(), required=False)
class WarnForm(BaseLogin):
"""
Bases: :class:`BaseLogin`
Form used on warn page before emiting a ticket
"""
#: ``True`` if the user has been warned of the ticket emission #: ``True`` if the user has been warned of the ticket emission
warned = forms.BooleanField(widget=forms.HiddenInput(), required=False) warned = forms.BooleanField(widget=forms.HiddenInput(), required=False)
#: A valid LoginTicket to prevent POST replay
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
class FederateSelect(BootsrapForm): class FederateSelect(BaseLogin):
""" """
Bases: :class:`django.forms.Form` Bases: :class:`BaseLogin`
Form used on the login page when ``settings.CAS_FEDERATE`` is ``True`` Form used on the login page when ``settings.CAS_FEDERATE`` is ``True``
allowing the user to choose an identity provider. allowing the user to choose an identity provider.
@ -76,39 +83,30 @@ class FederateSelect(BootsrapForm):
to_field_name="suffix", to_field_name="suffix",
label=_('Identity provider'), label=_('Identity provider'),
) )
#: The service url for which the user want a ticket #: A checkbox to ask to be warn before emiting a ticket for another service
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False) warn = forms.BooleanField(
method = forms.CharField(widget=forms.HiddenInput(), required=False) label=_('Warn me before logging me into other sites.'),
required=False
)
#: A checkbox to remember the user choices of :attr:`provider<FederateSelect.provider>` #: A checkbox to remember the user choices of :attr:`provider<FederateSelect.provider>`
remember = forms.BooleanField(label=_('Remember the identity provider'), required=False) remember = forms.BooleanField(label=_('Remember the identity provider'), required=False)
#: A checkbox to ask to be warn before emiting a ticket for another service
warn = forms.BooleanField(label=_('warn'), required=False)
#: Is the service asking the authentication renewal ?
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
class UserCredential(BootsrapForm): class UserCredential(BaseLogin):
""" """
Bases: :class:`django.forms.Form` Bases: :class:`BaseLogin`
Form used on the login page to retrive user credentials Form used on the login page to retrive user credentials
""" """
#: The user username #: The user username
username = forms.CharField(label=_('login')) username = forms.CharField(label=_('username'))
#: The service url for which the user want a ticket
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
#: The user password #: The user password
password = forms.CharField(label=_('password'), widget=forms.PasswordInput) password = forms.CharField(label=_('password'), widget=forms.PasswordInput)
#: A valid LoginTicket to prevent POST replay
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
method = forms.CharField(widget=forms.HiddenInput(), required=False)
#: A checkbox to ask to be warn before emiting a ticket for another service #: A checkbox to ask to be warn before emiting a ticket for another service
warn = forms.BooleanField(label=_('warn'), required=False) warn = forms.BooleanField(
#: Is the service asking the authentication renewal ? label=_('Warn me before logging me into other sites.'),
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False) required=False
)
def __init__(self, *args, **kwargs):
super(UserCredential, self).__init__(*args, **kwargs)
def clean(self): def clean(self):
""" """
@ -124,7 +122,9 @@ class UserCredential(BootsrapForm):
if auth.test_password(cleaned_data.get("password")): if auth.test_password(cleaned_data.get("password")):
cleaned_data["username"] = auth.username cleaned_data["username"] = auth.username
else: else:
raise forms.ValidationError(_(u"Bad user")) raise forms.ValidationError(
_(u"The credentials you provided cannot be determined to be authentic.")
)
return cleaned_data return cleaned_data
@ -148,21 +148,13 @@ class FederateUserCredential(UserCredential):
This stub authentication form, allow to implement the federated mode with very few This stub authentication form, allow to implement the federated mode with very few
modificatons to the :class:`LoginView<cas_server.views.LoginView>` view. modificatons to the :class:`LoginView<cas_server.views.LoginView>` view.
""" """
#: the user username with the ``@`` component
username = forms.CharField(widget=forms.HiddenInput()) def __init__(self, *args, **kwargs):
#: The service url for which the user want a ticket super(FederateUserCredential, self).__init__(*args, **kwargs)
service = forms.CharField(widget=forms.HiddenInput(), required=False) # All fields are hidden and auto filled by the /login view logic
#: The ``ticket`` used to authenticate the user against a provider for name, field in self.fields.items():
password = forms.CharField(widget=forms.HiddenInput()) field.widget = forms.HiddenInput()
#: alias of :attr:`password` self[name].display = False
ticket = forms.CharField(widget=forms.HiddenInput())
#: A valid LoginTicket to prevent POST replay
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
method = forms.CharField(widget=forms.HiddenInput(), required=False)
#: Has the user asked to be warn before emiting a ticket for another service
warn = forms.BooleanField(widget=forms.HiddenInput(), required=False)
#: Is the service asking the authentication renewal ?
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
def clean(self): def clean(self):
""" """

View File

@ -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."

View File

@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: cas_server\n" "Project-Id-Version: cas_server\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-07-04 17:36+0200\n" "POT-Creation-Date: 2016-08-01 12:01+0200\n"
"PO-Revision-Date: 2016-07-04 17:37+0200\n" "PO-Revision-Date: 2016-08-01 12:01+0200\n"
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n" "Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
"Language-Team: django <LL@li.org>\n" "Language-Team: django <LL@li.org>\n"
"Language: fr\n" "Language: fr\n"
@ -18,45 +18,45 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 1.8.8\n" "X-Generator: Poedit 1.8.8\n"
#: apps.py:19 templates/cas_server/base.html:3 #: apps.py:25 templates/cas_server/base.html:9
#: templates/cas_server/base.html:20 #: templates/cas_server/base.html:27
msgid "Central Authentication Service" msgid "Central Authentication Service"
msgstr "Service Central d'Authentification" msgstr "Service Central d'Authentification"
#: forms.py:43 #: forms.py:88
msgid "Identity provider" msgid "Identity provider"
msgstr "fournisseur d'identité" msgstr "fournisseur d'identité"
#: forms.py:45 forms.py:55 forms.py:106 #: forms.py:92 forms.py:111
msgid "service" msgid "Warn me before logging me into other sites."
msgstr "service" msgstr "Prévenez-moi avant d'accéder à d'autres services."
#: forms.py:47 #: forms.py:96
msgid "Remember the identity provider" msgid "Remember the identity provider"
msgstr "Se souvenir du fournisseur d'identité" msgstr "Se souvenir du fournisseur d'identité"
#: forms.py:48 forms.py:59 #: forms.py:106 models.py:600
msgid "warn" msgid "username"
msgstr "Prévenez-moi avant d'accéder à d'autres services." msgstr "nom d'utilisateur"
#: forms.py:54 #: forms.py:108
msgid "login"
msgstr "Identifiant"
#: forms.py:56
msgid "password" msgid "password"
msgstr "mot de passe" msgstr "mot de passe"
#: forms.py:71 #: forms.py:130
msgid "Bad user" msgid "The credentials you provided cannot be determined to be authentic."
msgstr "Les informations transmises n'ont pas permis de vous authentifier." msgstr "Les informations transmises n'ont pas permis de vous authentifier."
#: forms.py:96 #: forms.py:182
msgid "User not found in the temporary database, please try to reconnect" msgid "User not found in the temporary database, please try to reconnect"
msgstr "" msgstr ""
"Utilisateur non trouvé dans la base de donnée temporaire, essayez de vous " "Utilisateur non trouvé dans la base de donnée temporaire, essayez de vous "
"reconnecter" "reconnecter"
#: forms.py:196
msgid "service"
msgstr "service"
#: management/commands/cas_clean_federate.py:20 #: management/commands/cas_clean_federate.py:20
msgid "Clean old federated users" msgid "Clean old federated users"
msgstr "Nettoyer les anciens utilisateurs fédéré" msgstr "Nettoyer les anciens utilisateurs fédéré"
@ -69,98 +69,99 @@ msgstr "Nettoyer les sessions supprimées"
msgid "Clean old trickets" msgid "Clean old trickets"
msgstr "Nettoyer les vieux tickets" msgstr "Nettoyer les vieux tickets"
#: models.py:42 #: models.py:46
msgid "identity provider" msgid "identity provider"
msgstr "fournisseur d'identité" msgstr "fournisseur d'identité"
#: models.py:43 #: models.py:47
msgid "identity providers" msgid "identity providers"
msgstr "fournisseurs d'identités" msgstr "fournisseurs d'identités"
#: models.py:47 #: models.py:53
msgid "suffix" msgid "suffix"
msgstr "suffixe" msgstr "suffixe"
#: models.py:48 #: models.py:55
msgid "" msgid ""
"Suffix append to backend CAS returner username: `returned_username`@`suffix`" "Suffix append to backend CAS returned username: ``returned_username`` @ "
"``suffix``."
msgstr "" msgstr ""
"Suffixe ajouté au nom d'utilisateur retourné par le CAS du fournisseur " "Suffixe ajouté au nom d'utilisateur retourné par le CAS du fournisseur "
"d'identité : `nom retourné`@`suffixe`" "d'identité : `nom retourné`@`suffixe`."
#: models.py:50 #: models.py:62
msgid "server url" msgid "server url"
msgstr "url du serveur" msgstr "url du serveur"
#: models.py:59 #: models.py:72
msgid "CAS protocol version" msgid "CAS protocol version"
msgstr "Version du protocole CAS" msgstr "Version du protocole CAS"
#: models.py:60 #: models.py:74
msgid "" msgid ""
"Version of the CAS protocol to use when sending requests the the backend CAS" "Version of the CAS protocol to use when sending requests the the backend CAS."
msgstr "" msgstr ""
"Version du protocole CAS à utiliser lorsque l'on envoie des requête au CAS " "Version du protocole CAS à utiliser lorsque l'on envoie des requête au CAS "
"du fournisseur d'identité" "du fournisseur d'identité."
#: models.py:65 #: models.py:81
msgid "verbose name" msgid "verbose name"
msgstr "Nom du fournisseur" msgstr "Nom du fournisseur"
#: models.py:66 #: models.py:82
msgid "Name for this identity provider displayed on the login page" msgid "Name for this identity provider displayed on the login page."
msgstr "Nom affiché pour ce fournisseur d'identité sur la page de connexion" msgstr "Nom affiché pour ce fournisseur d'identité sur la page de connexion."
#: models.py:70 models.py:317 #: models.py:88 models.py:446
msgid "position" msgid "position"
msgstr "position" msgstr "position"
#: models.py:80 #: models.py:102
msgid "display" msgid "display"
msgstr "afficher" msgstr "afficher"
#: models.py:81 #: models.py:103
msgid "Display the provider on the login page" msgid "Display the provider on the login page."
msgstr "Afficher le fournisseur d'identité sur la page de connexion" msgstr "Afficher le fournisseur d'identité sur la page de connexion."
#: models.py:164 #: models.py:233
msgid "User" msgid "User"
msgstr "Utilisateur" msgstr "Utilisateur"
#: models.py:165 #: models.py:234
msgid "Users" msgid "Users"
msgstr "Utilisateurs" msgstr "Utilisateurs"
#: models.py:234 #: models.py:320
#, python-format #, python-format
msgid "Error during service logout %s" msgid "Error during service logout %s"
msgstr "Une erreur est survenue durant la déconnexion du service %s" msgstr "Une erreur est survenue durant la déconnexion du service %s"
#: models.py:312 #: models.py:440
msgid "Service pattern" msgid "Service pattern"
msgstr "Motif de service" msgstr "Motif de service"
#: models.py:313 #: models.py:441
msgid "Services patterns" msgid "Services patterns"
msgstr "Motifs de services" msgstr "Motifs de services"
#: models.py:318 #: models.py:447
msgid "service patterns are sorted using the position attribute" msgid "service patterns are sorted using the position attribute"
msgstr "Les motifs de service sont trié selon l'attribut position" msgstr "Les motifs de service sont trié selon l'attribut position"
#: models.py:325 models.py:449 #: models.py:455 models.py:626
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
#: models.py:326 #: models.py:456
msgid "A name for the service" msgid "A name for the service"
msgstr "Un nom pour le service" msgstr "Un nom pour le service"
#: models.py:331 models.py:478 models.py:497 #: models.py:464 models.py:669 models.py:698
msgid "pattern" msgid "pattern"
msgstr "motif" msgstr "motif"
#: models.py:333 #: models.py:466
msgid "" msgid ""
"A regular expression matching services. Will usually looks like '^https://" "A regular expression matching services. Will usually looks like '^https://"
"some\\.server\\.com/path/.*$'.As it is a regular expression, special " "some\\.server\\.com/path/.*$'.As it is a regular expression, special "
@ -171,55 +172,55 @@ msgstr ""
"expression rationnelle, les caractères spéciaux doivent être échappés avec " "expression rationnelle, les caractères spéciaux doivent être échappés avec "
"un '\\'." "un '\\'."
#: models.py:342 #: models.py:476
msgid "user field" msgid "user field"
msgstr "champ utilisateur" msgstr "champ utilisateur"
#: models.py:343 #: models.py:477
msgid "Name of the attribut to transmit as username, empty = login" msgid "Name of the attribute to transmit as username, empty = login"
msgstr "" msgstr ""
"Nom de l'attribut devant être transmis comme nom d'utilisateur au service. " "Nom de l'attribut devant être transmis comme nom d'utilisateur au service. "
"vide = nom de connection" "vide = nom de connexion"
#: models.py:347 #: models.py:482
msgid "restrict username" msgid "restrict username"
msgstr "limiter les noms d'utilisateurs" msgstr "limiter les noms d'utilisateurs"
#: models.py:348 #: models.py:483
msgid "Limit username allowed to connect to the list provided bellow" msgid "Limit username allowed to connect to the list provided bellow"
msgstr "" msgstr ""
"Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie " "Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie "
"ci-dessous" "ci-dessous"
#: models.py:352 #: models.py:488
msgid "proxy" msgid "proxy"
msgstr "proxy" msgstr "proxy"
#: models.py:353 #: models.py:489
msgid "Proxy tickets can be delivered to the service" msgid "Proxy tickets can be delivered to the service"
msgstr "des proxy tickets peuvent être délivrés au service" msgstr "des proxy tickets peuvent être délivrés au service"
#: models.py:357 #: models.py:495
msgid "proxy callback" msgid "proxy callback"
msgstr "" msgstr ""
#: models.py:358 #: models.py:496
msgid "can be used as a proxy callback to deliver PGT" msgid "can be used as a proxy callback to deliver PGT"
msgstr "peut être utilisé comme un callback pour recevoir un PGT" msgstr "peut être utilisé comme un callback pour recevoir un PGT"
#: models.py:362 #: models.py:503
msgid "single log out" msgid "single log out"
msgstr "" msgstr ""
#: models.py:363 #: models.py:504
msgid "Enable SLO for the service" msgid "Enable SLO for the service"
msgstr "Active le SLO pour le service" msgstr "Active le SLO pour le service"
#: models.py:370 #: models.py:512
msgid "single log out callback" msgid "single log out callback"
msgstr "" msgstr ""
#: models.py:371 #: models.py:513
msgid "" msgid ""
"URL where the SLO request will be POST. empty = service url\n" "URL where the SLO request will be POST. empty = service url\n"
"This is usefull for non HTTP proxied services." "This is usefull for non HTTP proxied services."
@ -228,83 +229,98 @@ msgstr ""
"service\n" "service\n"
"Ceci n'est utilise que pour des services non HTTP proxifiés" "Ceci n'est utilise que pour des services non HTTP proxifiés"
#: models.py:433 #: models.py:601
msgid "username"
msgstr "nom d'utilisateur"
#: models.py:434
msgid "username allowed to connect to the service" msgid "username allowed to connect to the service"
msgstr "noms d'utilisateurs autorisé à se connecter au service" msgstr "noms d'utilisateurs autorisé à se connecter au service"
#: models.py:450 #: models.py:627
msgid "name of an attribut to send to the service, use * for all attributes" msgid "name of an attribute to send to the service, use * for all attributes"
msgstr "" msgstr ""
"nom d'un attribut a envoyer au service, utiliser * pour tous les attributs" "nom d'un attribut a envoyer au service, utiliser * pour tous les attributs"
#: models.py:455 models.py:503 #: models.py:634 models.py:705
msgid "replace" msgid "replace"
msgstr "remplacement" msgstr "remplacement"
#: models.py:456 #: models.py:635
msgid "" msgid ""
"name under which the attribut will be showto the service. empty = default " "name under which the attribute will be showto the service. empty = default "
"name of the attribut" "name of the attribut"
msgstr "" msgstr ""
"nom sous lequel l'attribut sera rendu visible au service. vide = inchangé" "nom sous lequel l'attribut sera rendu visible au service. vide = inchangé"
#: models.py:473 models.py:492 #: models.py:662 models.py:692
msgid "attribut" msgid "attribute"
msgstr "attribut" msgstr "attribut"
#: models.py:474 #: models.py:663
msgid "Name of the attribut which must verify pattern" msgid "Name of the attribute which must verify pattern"
msgstr "Nom de l'attribut devant vérifier un motif" msgstr "Nom de l'attribut devant vérifier un motif"
#: models.py:479 #: models.py:670
msgid "a regular expression" msgid "a regular expression"
msgstr "une expression régulière" msgstr "une expression régulière"
#: models.py:493 #: models.py:693
msgid "Name of the attribut for which the value must be replace" msgid "Name of the attribute for which the value must be replace"
msgstr "nom de l'attribue pour lequel la valeur doit être remplacé" msgstr "nom de l'attribut pour lequel la valeur doit être remplacé"
#: models.py:498 #: models.py:699
msgid "An regular expression maching whats need to be replaced" msgid "An regular expression maching whats need to be replaced"
msgstr "une expression régulière reconnaissant ce qui doit être remplacé" msgstr "une expression régulière reconnaissant ce qui doit être remplacé"
#: models.py:504 #: models.py:706
msgid "replace expression, groups are capture by \\1, \\2 …" msgid "replace expression, groups are capture by \\1, \\2 …"
msgstr "expression de remplacement, les groupe sont capturé par \\1, \\2" msgstr "expression de remplacement, les groupe sont capturé par \\1, \\2"
#: templates/cas_server/logged.html:6 #: templates/cas_server/base.html:38
msgid "Logged" #, python-format
msgid ""
"A new version of the application is available. This instance runs "
"%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider "
"upgrading."
msgstr "" msgstr ""
"<h3>Connexion réussie</h3>Vous vous êtes authentifié(e) auprès du Service " "Une nouvelle version de l'application est disponible. Cette instance utilise "
"Central d'Authentification.<br/>Pour des raisons de sécurité, veuillez vous " "la version %(VERSION)s et la dernière version est %(LAST_VERSION)s. Merci de "
"déconnecter et fermer votre navigateur lorsque vous avez fini d'accéder aux " "vous mettre a jour."
"services authentifiés."
#: templates/cas_server/logged.html:10 #: templates/cas_server/logged.html:4
msgid ""
"<h3>Log In Successful</h3>You have successfully logged into the Central "
"Authentication Service.<br/>For security reasons, please Log Out and Exit "
"your web browser when you are done accessing services that require "
"authentication!"
msgstr ""
"<h3>Déconnexion réussie</h3>Vous vous êtes déconnecté(e) du Service Central "
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
"navigateur après avoir fini d'accéder a des services demandant une "
"authentification !"
#: templates/cas_server/logged.html:8
msgid "Log me out from all my sessions" msgid "Log me out from all my sessions"
msgstr "Me déconnecter de toutes mes sessions" msgstr "Me déconnecter de toutes mes sessions"
#: templates/cas_server/logged.html:13 #: templates/cas_server/logged.html:14
msgid "Forget the identity provider"
msgstr "Oublier le fournisseur d'identité"
#: templates/cas_server/logged.html:18
msgid "Logout" msgid "Logout"
msgstr "Se déconnecter" msgstr "Se déconnecter"
#: templates/cas_server/login.html:8 #: templates/cas_server/login.html:6
msgid "Please log in" msgid "Please log in"
msgstr "Merci de se connecter" msgstr "Merci de se connecter"
#: templates/cas_server/login.html:13 #: templates/cas_server/login.html:14
msgid "Login" msgid "Login"
msgstr "Connexion" msgstr "Connexion"
#: templates/cas_server/warn.html:10 #: templates/cas_server/warn.html:9
msgid "Connect to the service" msgid "Connect to the service"
msgstr "Se connecter au service" msgstr "Se connecter au service"
#: views.py:152 #: views.py:168
msgid "" msgid ""
"<h3>Logout successful</h3>You have successfully logged out from the Central " "<h3>Logout successful</h3>You have successfully logged out from the Central "
"Authentication Service. For security reasons, exit your web browser." "Authentication Service. For security reasons, exit your web browser."
@ -313,7 +329,7 @@ msgstr ""
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre " "d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
"navigateur." "navigateur."
#: views.py:158 #: views.py:174
#, python-format #, python-format
msgid "" msgid ""
"<h3>Logout successful</h3>You have successfully logged out from %s sessions " "<h3>Logout successful</h3>You have successfully logged out from %s sessions "
@ -324,7 +340,7 @@ msgstr ""
"Service Central d'Authentification. Pour des raisons de sécurité, veuillez " "Service Central d'Authentification. Pour des raisons de sécurité, veuillez "
"fermer votre navigateur." "fermer votre navigateur."
#: views.py:165 #: views.py:181
msgid "" msgid ""
"<h3>Logout successful</h3>You were already logged out from the Central " "<h3>Logout successful</h3>You were already logged out from the Central "
"Authentication Service. For security reasons, exit your web browser." "Authentication Service. For security reasons, exit your web browser."
@ -333,50 +349,75 @@ msgstr ""
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre " "d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
"navigateur." "navigateur."
#: views.py:349 #: views.py:361
msgid "Invalid login ticket" #, python-format
msgid ""
"Invalid response from your identity provider CAS upon ticket %(ticket)s "
"validation: %(error)r"
msgstr ""
"Réponse invalide du CAS du fournisseur d'identité lors de la validation du "
"ticket %(ticket)s: %(error)r"
#: views.py:483
msgid "Invalid login ticket, please retry to login"
msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter" msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter"
#: views.py:470 #: views.py:675
#, python-format #, python-format
msgid "Authentication has been required by service %(name)s (%(url)s)" msgid "Authentication has been required by service %(name)s (%(url)s)"
msgstr "" msgstr ""
"Une demande d'authentification a été émise pour le service %(name)s " "Une demande d'authentification a été émise pour le service %(name)s "
"(%(url)s)." "(%(url)s)."
#: views.py:508 #: views.py:713
#, python-format #, python-format
msgid "Service %(url)s non allowed." msgid "Service %(url)s non allowed."
msgstr "le service %(url)s n'est pas autorisé." msgstr "le service %(url)s n'est pas autorisé."
#: views.py:515 #: views.py:720
msgid "Username non allowed" msgid "Username non allowed"
msgstr "Nom d'utilisateur non authorisé" msgstr "Nom d'utilisateur non authorisé"
#: views.py:522 #: views.py:727
msgid "User charateristics non allowed" msgid "User characteristics non allowed"
msgstr "Caractéristique utilisateur non autorisée" msgstr "Caractéristique utilisateur non autorisée"
#: views.py:529 #: views.py:734
#, python-format #, python-format
msgid "The attribut %(field)s is needed to use that service" msgid "The attribute %(field)s is needed to use that service"
msgstr "L'attribut %(field)s est nécessaire pour se connecter à ce service" msgstr "L'attribut %(field)s est nécessaire pour se connecter à ce service"
#: views.py:599 #: views.py:824
#, python-format #, python-format
msgid "Authentication renewal required by service %(name)s (%(url)s)." msgid "Authentication renewal required by service %(name)s (%(url)s)."
msgstr "Demande de réauthentification pour le service %(name)s (%(url)s)." msgstr "Demande de réauthentification pour le service %(name)s (%(url)s)."
#: views.py:606 #: views.py:831
#, python-format #, python-format
msgid "Authentication required by service %(name)s (%(url)s)." msgid "Authentication required by service %(name)s (%(url)s)."
msgstr "Authentification requise par le service %(name)s (%(url)s)." msgstr "Authentification requise par le service %(name)s (%(url)s)."
#: views.py:613 #: views.py:838
#, python-format #, python-format
msgid "Service %s non allowed" msgid "Service %s non allowed"
msgstr "Le service %s n'est pas autorisé" msgstr "Le service %s n'est pas autorisé"
#~ msgid "Logged"
#~ msgstr ""
#~ "<h3>Connexion réussie</h3>Vous vous êtes authentifié(e) auprès du Service "
#~ "Central d'Authentification.<br/>Pour des raisons de sécurité, veuillez "
#~ "vous déconnecter et fermer votre navigateur lorsque vous avez fini "
#~ "d'accéder aux services authentifiés."
#~ msgid "warn"
#~ msgstr "Prévenez-moi avant d'accéder à d'autres services."
#~ msgid "login"
#~ msgstr "Identifiant"
#~ msgid "Bad user"
#~ msgstr "Les informations transmises n'ont pas permis de vous authentifier."
#~ msgid "" #~ msgid ""
#~ "Error during service logout %(service)s:\n" #~ "Error during service logout %(service)s:\n"
#~ "%(error)s" #~ "%(error)s"

View File

@ -23,3 +23,4 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
models.User.clean_deleted_sessions() models.User.clean_deleted_sessions()
models.NewVersionWarning.send_mails()

View 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)),
],
),
]

View File

@ -18,15 +18,18 @@ from django.contrib import messages
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.core.mail import send_mail
import re import re
import sys import sys
import smtplib
import logging import logging
from datetime import timedelta from datetime import timedelta
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from requests_futures.sessions import FuturesSession from requests_futures.sessions import FuturesSession
import cas_server.utils as utils import cas_server.utils as utils
from . import VERSION
#: logger facility #: logger facility
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -465,13 +468,13 @@ class ServicePattern(models.Model):
"As it is a regular expression, special character must be escaped with a '\\'." "As it is a regular expression, special character must be escaped with a '\\'."
) )
) )
#: Name of the attribut to transmit as username, if empty the user login is used #: Name of the attribute to transmit as username, if empty the user login is used
user_field = models.CharField( user_field = models.CharField(
max_length=255, max_length=255,
default="", default="",
blank=True, blank=True,
verbose_name=_(u"user field"), verbose_name=_(u"user field"),
help_text=_("Name of the attribut to transmit as username, empty = login") help_text=_("Name of the attribute to transmit as username, empty = login")
) )
#: A boolean allowing to limit username allowed to connect to :attr:`usernames`. #: A boolean allowing to limit username allowed to connect to :attr:`usernames`.
restrict_users = models.BooleanField( restrict_users = models.BooleanField(
@ -621,7 +624,7 @@ class ReplaceAttributName(models.Model):
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
verbose_name=_(u"name"), verbose_name=_(u"name"),
help_text=_(u"name of an attribut to send to the service, use * for all attributes") help_text=_(u"name of an attribute to send to the service, use * for all attributes")
) )
#: The name of the attribute to transmit to the service. If empty, the value of :attr:`name` #: The name of the attribute to transmit to the service. If empty, the value of :attr:`name`
#: is used. #: is used.
@ -629,7 +632,7 @@ class ReplaceAttributName(models.Model):
max_length=255, max_length=255,
blank=True, blank=True,
verbose_name=_(u"replace"), verbose_name=_(u"replace"),
help_text=_(u"name under which the attribut will be show" help_text=_(u"name under which the attribute will be show"
u"to the service. empty = default name of the attribut") u"to the service. empty = default name of the attribut")
) )
#: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributName` instances for a #: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributName` instances for a
@ -656,8 +659,8 @@ class FilterAttributValue(models.Model):
#: The name of a user attribute #: The name of a user attribute
attribut = models.CharField( attribut = models.CharField(
max_length=255, max_length=255,
verbose_name=_(u"attribut"), verbose_name=_(u"attribute"),
help_text=_(u"Name of the attribut which must verify pattern") help_text=_(u"Name of the attribute which must verify pattern")
) )
#: A regular expression the attribute :attr:`attribut` value must verify. If :attr:`attribut` #: A regular expression the attribute :attr:`attribut` value must verify. If :attr:`attribut`
#: if a list, only one of the list values needs to match. #: if a list, only one of the list values needs to match.
@ -686,8 +689,8 @@ class ReplaceAttributValue(models.Model):
#: Name the attribute: a key of :attr:`User.attributs` #: Name the attribute: a key of :attr:`User.attributs`
attribut = models.CharField( attribut = models.CharField(
max_length=255, max_length=255,
verbose_name=_(u"attribut"), verbose_name=_(u"attribute"),
help_text=_(u"Name of the attribut for which the value must be replace") help_text=_(u"Name of the attribute for which the value must be replace")
) )
#: A regular expression matching the part of the attribute value that need to be changed #: A regular expression matching the part of the attribute value that need to be changed
pattern = models.CharField( pattern = models.CharField(
@ -1003,3 +1006,60 @@ class Proxy(models.Model):
def __str__(self): def __str__(self):
return self.url return self.url
class NewVersionWarning(models.Model):
"""
Bases: :class:`django.db.models.Model`
The last new version available version sent
"""
version = models.CharField(max_length=255)
@classmethod
def send_mails(cls):
"""
For each new django-cas-server version, if the current instance is not up to date
send one mail to ``settings.ADMINS``.
"""
if settings.CAS_NEW_VERSION_EMAIL_WARNING and settings.ADMINS:
try:
obj = cls.objects.get()
except cls.DoesNotExist:
obj = NewVersionWarning.objects.create(version=VERSION)
LAST_VERSION = utils.last_version()
if LAST_VERSION is not None and LAST_VERSION != obj.version:
if utils.decode_version(VERSION) < utils.decode_version(LAST_VERSION):
try:
send_mail(
(
'%sA new version of django-cas-server is available'
) % settings.EMAIL_SUBJECT_PREFIX,
u'''
A new version of the django-cas-server is available.
Your version: %s
New version: %s
Upgrade using:
* pip install -U django-cas-server
* fetching the last release on
https://github.com/nitmir/django-cas-server/ or on
https://pypi.python.org/pypi/django-cas-server
After upgrade, do not forget to run:
* ./manage.py migrate
* ./manage.py collectstatic
and to reload your wsgi server (apache2, uwsgi, gunicord, etc)
--\u0020
django-cas-server
'''.strip() % (VERSION, LAST_VERSION),
settings.SERVER_EMAIL,
["%s <%s>" % admin for admin in settings.ADMINS],
fail_silently=False,
)
obj.version = LAST_VERSION
obj.save()
except smtplib.SMTPException as error: # pragma: no cover (should not happen)
logger.error("Unable to send new version mail: %s" % error)

View 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();
}
});
}

View File

@ -14,8 +14,8 @@
<script src="{{settings.CAS_COMPONENT_URLS.html5shiv}}"></script> <script src="{{settings.CAS_COMPONENT_URLS.html5shiv}}"></script>
<script src="{{settings.CAS_COMPONENT_URLS.respond}}"></script> <script src="{{settings.CAS_COMPONENT_URLS.respond}}"></script>
<![endif]--> <![endif]-->
<link rel="shortcut icon" href="{% static "cas_server/favicon.ico?v=1" %}" /> {% if settings.CAS_FAVICON_URL %}<link rel="shortcut icon" href="{{settings.CAS_FAVICON_URL}}" />{% endif %}
<link href="{% static "cas_server/login.css" %}" rel="stylesheet"> <link href="{% static "cas_server/styles.css" %}" rel="stylesheet">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@ -31,23 +31,28 @@
<div class="row"> <div class="row">
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div> <div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12"> <div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
{% block ante_messages %}{% endblock %}
{% if auto_submit %}<noscript>{% endif %} {% if auto_submit %}<noscript>{% endif %}
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
<div class="alert alert-info alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="alert-version">&#215;</button>
{% blocktrans %}A new version of the application is available. This instance runs {{VERSION}} and the last version is {{LAST_VERSION}}. Please consider upgrading.{% endblocktrans %}
</div>
{% endif %}
{% block ante_messages %}{% endblock %}
{% for message in messages %} {% for message in messages %}
<div {% spaceless %} <div {% spaceless %}
{% if message.level == message_levels.DEBUG %} {% if message.level == message_levels.DEBUG %}
class="alert alert-warning alert-dismissable" class="alert alert-warning"
{% elif message.level == message_levels.INFO %} {% elif message.level == message_levels.INFO %}
class="alert alert-info alert-dismissable" class="alert alert-info"
{% elif message.level == message_levels.SUCCESS %} {% elif message.level == message_levels.SUCCESS %}
class="alert alert-success alert-dismissable" class="alert alert-success"
{% elif message.level == message_levels.WARNING %} {% elif message.level == message_levels.WARNING %}
class="alert alert-warning alert-dismissable" class="alert alert-warning"
{% else %} {% else %}
class="alert alert-danger alert-dismissable" class="alert alert-danger"
{% endif %} {% endif %}
{% endspaceless %}> {% endspaceless %}>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&#215;</button>
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
@ -59,5 +64,9 @@
</div> <!-- /container --> </div> <!-- /container -->
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script> <script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script> <script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
<script src="{% static "cas_server/alert-version.js" %}"></script>
<script>alert_version("{{LAST_VERSION}}")</script>
{% endif %}
</body> </body>
</html> </html>

View File

@ -1,10 +1,11 @@
{% load cas_server %}
{% for error in form.non_field_errors %} {% for error in form.non_field_errors %}
<div class="alert alert-danger alert-dismissable"> <div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&#215;</button> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&#215;</button>
{{error}} {{error}}
</div> </div>
{% endfor %} {% endfor %}
{% for field in form %}{% if field.display %} {% for field in form %}{% if not field|is_hidden %}
<div class="form-group{% spaceless %} <div class="form-group{% spaceless %}
{% if not form.non_field_errors %} {% if not form.non_field_errors %}
{% if field.errors %} has-error {% if field.errors %} has-error
@ -12,7 +13,7 @@
{% endif %} {% endif %}
{% endif %}" {% endif %}"
{% endspaceless %}>{% spaceless %} {% endspaceless %}>{% spaceless %}
{% if field.checkbox %} {% if field|is_checkbox %}
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label> <div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label>
{% else %} {% else %}
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label> <label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>

View File

@ -1,13 +1,20 @@
{% extends "cas_server/base.html" %} {% extends "cas_server/base.html" %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="alert alert-success" role="alert">{% trans "Logged" %}</div> <div class="alert alert-success" role="alert">{% blocktrans %}<h3>Log In Successful</h3>You have successfully logged into the Central Authentication Service.<br/>For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!{% endblocktrans %}</div>
<form class="form-signin" method="get" action="logout"> <form class="form-signin" method="get" action="logout">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" name="all" value="1"> {% trans "Log me out from all my sessions" %} <input type="checkbox" name="all" value="1">{% trans "Log me out from all my sessions" %}
</label> </label>
</div> </div>
{% if settings.CAS_FEDERATE and request.COOKIES.remember_provider %}
<div class="checkbox">
<label>
<input type="checkbox" name="forget_provider" value="1">{% trans "Forget the identity provider" %}
</label>
</div>
{% endif %}
<button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button> <button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button>
</form> </form>
{% endblock %} {% endblock %}

View File

View 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)

View File

@ -51,6 +51,22 @@ MIDDLEWARE_CLASSES = [
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
] ]
TEMPLATES = [
{
'APP_DIRS': True,
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages'
]
}
}
]
ROOT_URLCONF = 'cas_server.tests.urls' ROOT_URLCONF = 'cas_server.tests.urls'
# Database # Database
@ -81,3 +97,30 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.9/howto/static-files/ # https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
CAS_NEW_VERSION_HTML_WARNING = False
CAS_NEW_VERSION_EMAIL_WARNING = False
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'cas_file': {
'format': '%(asctime)s %(levelname)s %(message)s'
},
},
'handlers': {
'cas_stream': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'cas_file',
},
},
'loggers': {
'cas_server': {
'handlers': ['cas_stream'],
'level': 'INFO',
'propagate': True,
},
},
}

View File

@ -84,29 +84,31 @@ class FederateAuthLoginLogoutTestCase(
params['provider'] = provider.suffix params['provider'] = provider.suffix
if remember: if remember:
params['remember'] = 'on' params['remember'] = 'on'
# just try for one suffix
if suffix == "example.com":
# if renew=False is posted it should be ignored
params["renew"] = False
# post the choosed provider # post the choosed provider
response = client.post('/federate', params) response = client.post('/federate', params)
# we are redirected to the provider CAS client url # we are redirected to the provider CAS client url
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
if remember: self.assertEqual(response["Location"], '%s/federate/%s%s' % (
self.assertEqual(response["Location"], '%s/federate/%s?remember=on' % (
'http://testserver' if django.VERSION < (1, 9) else "", 'http://testserver' if django.VERSION < (1, 9) else "",
provider.suffix provider.suffix,
)) "?remember=on" if remember else ""
else:
self.assertEqual(response["Location"], '%s/federate/%s' % (
'http://testserver' if django.VERSION < (1, 9) else "",
provider.suffix
)) ))
# let's follow the redirect # let's follow the redirect
response = client.get('/federate/%s' % provider.suffix) response = client.get(
'/federate/%s%s' % (provider.suffix, "?remember=on" if remember else "")
)
# we are redirected to the provider CAS for authentication # we are redirected to the provider CAS for authentication
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual( self.assertEqual(
response["Location"], response["Location"],
"%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % ( "%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s%s" % (
provider.server_url, provider.server_url,
provider.suffix provider.suffix,
"%3Fremember%3Don" if remember else ""
) )
) )
# let's generate a ticket # let's generate a ticket
@ -114,7 +116,10 @@ class FederateAuthLoginLogoutTestCase(
# we lauch a dummy CAS server that only validate once for the service # we lauch a dummy CAS server that only validate once for the service
# http://testserver/federate/example.com with `ticket` # http://testserver/federate/example.com with `ticket`
tests_utils.DummyCAS.run( tests_utils.DummyCAS.run(
("http://testserver/federate/%s" % provider.suffix).encode("ascii"), ("http://testserver/federate/%s%s" % (
provider.suffix,
"?remember=on" if remember else ""
)).encode("ascii"),
ticket.encode("ascii"), ticket.encode("ascii"),
settings.CAS_TEST_USER.encode("utf8"), settings.CAS_TEST_USER.encode("utf8"),
[], [],
@ -122,7 +127,13 @@ class FederateAuthLoginLogoutTestCase(
) )
# we normally provide a good ticket and should be redirected to /login as the ticket # we normally provide a good ticket and should be redirected to /login as the ticket
# get successfully validated again the dummy CAS # get successfully validated again the dummy CAS
response = client.get('/federate/%s' % provider.suffix, {'ticket': ticket}) response = client.get(
'/federate/%s' % provider.suffix,
{'ticket': ticket, 'remember': 'on' if remember else ''}
)
if remember:
self.assertIn("remember_provider", client.cookies)
self.assertEqual(client.cookies["remember_provider"].value, provider.suffix)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], "%s/login" % ( self.assertEqual(response["Location"], "%s/login" % (
'http://testserver' if django.VERSION < (1, 9) else "" 'http://testserver' if django.VERSION < (1, 9) else ""
@ -183,7 +194,8 @@ class FederateAuthLoginLogoutTestCase(
""" """
The federated view should redirect to /login if the provider is unknown or not provided, The federated view should redirect to /login if the provider is unknown or not provided,
try to fetch a new ticket if the provided ticket validation fail try to fetch a new ticket if the provided ticket validation fail
(network error or bad ticket) (network error or bad ticket), redirect to /login with a error message if identity
provider CAS return a bad response (invalid XML document)
""" """
good_provider = "example.com" good_provider = "example.com"
bad_provider = "exemple.fr" bad_provider = "exemple.fr"
@ -229,6 +241,18 @@ class FederateAuthLoginLogoutTestCase(
'http://testserver' if django.VERSION < (1, 9) else "" 'http://testserver' if django.VERSION < (1, 9) else ""
)) ))
# test CAS avaible but return a bad XML doc, should redirect to /login with a error message
# use "example.net" as it is CASv3
tests_utils.HttpParamsHandler.run(8082)
response = client.get("/federate/%s" % "example.net", {'ticket': utils.gen_st()})
self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], "%s/login" % (
'http://testserver' if django.VERSION < (1, 9) else ""
))
response = client.get("/login")
self.assertEqual(response.status_code, 200)
self.assertIn(b"Invalid response from your identity provider CAS", response.content)
def test_auth_federate_slo(self): def test_auth_federate_slo(self):
"""test that SLO receive from backend CAS log out the users""" """test that SLO receive from backend CAS log out the users"""
# get tickets and connected clients # get tickets and connected clients
@ -331,6 +355,76 @@ class FederateAuthLoginLogoutTestCase(
provider.suffix provider.suffix
)) ))
def test_forget_provider(self):
"""Test the logout option to forget remembered provider"""
tickets = self.test_login_post_provider(remember=True)
for (provider, _, client) in tickets:
self.assertIn("remember_provider", client.cookies)
self.assertEqual(client.cookies["remember_provider"].value, provider.suffix)
self.assertNotEqual(client.cookies["remember_provider"]["max-age"], 0)
client.get("/logout?forget_provider=1")
self.assertEqual(client.cookies["remember_provider"]["max-age"], 0)
def test_renew(self):
"""
Test authentication renewal with federation mode
"""
tickets = self.test_login_post_provider()
for (provider, _, client) in tickets:
# Try to renew authentication(client already authenticated in test_login_post_provider
response = client.get("/login?renew=true")
# we should be redirected to the user CAS
self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], "%s/federate/%s?renew=true" % (
'http://testserver' if django.VERSION < (1, 9) else "",
provider.suffix
))
response = client.get("/federate/%s?renew=true" % provider.suffix)
self.assertEqual(response.status_code, 302)
service_url = (
"service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s%%3Frenew%%3Dtrue"
) % provider.suffix
self.assertIn(service_url, response["Location"])
self.assertIn("renew=true", response["Location"])
cas_port = int(provider.server_url.split(':')[-1])
# let's generate a ticket
ticket = utils.gen_st()
# we lauch a dummy CAS server that only validate once for the service
# http://testserver/federate/example.com?renew=true with `ticket`
tests_utils.DummyCAS.run(
("http://testserver/federate/%s?renew=true" % provider.suffix).encode("ascii"),
ticket.encode("ascii"),
settings.CAS_TEST_USER.encode("utf8"),
[],
cas_port
)
# we normally provide a good ticket and should be redirected to /login as the ticket
# get successfully validated again the dummy CAS
response = client.get(
'/federate/%s' % provider.suffix,
{'ticket': ticket, 'renew': 'true'}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], "%s/login?renew=true" % (
'http://testserver' if django.VERSION < (1, 9) else ""
))
# follow the redirect and try to get a ticket to see is it has renew set to True
response = client.get("/login?renew=true&service=%s" % self.service)
# we should get a page with a from with all widget hidden that auto POST to /login using
# javascript. If javascript is disabled, a "connect" button is showed
self.assertTrue(response.context['auto_submit'])
self.assertEqual(response.context['post_url'], '/login')
params = tests_utils.copy_form(response.context["form"])
# POST get prefiled from parameters
response = client.post("/login", params)
self.assertEqual(response.status_code, 302)
self.assertTrue(response["Location"].startswith("%s?ticket=" % self.service))
ticket_value = response["Location"].split('ticket=')[-1]
ticket = models.ServiceTicket.objects.get(value=ticket_value)
self.assertTrue(ticket.renew)
def test_login_bad_ticket(self): def test_login_bad_ticket(self):
""" """
Try login with a bad ticket: Try login with a bad ticket:

View File

@ -16,7 +16,9 @@ import django
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import timezone from django.utils import timezone
from django.core import mail
import mock
from datetime import timedelta from datetime import timedelta
from importlib import import_module from importlib import import_module
@ -60,6 +62,25 @@ class FederatedUserTestCase(TestCase, UserModels, FederatedIendityProviderModel)
with self.assertRaises(models.FederatedUser.DoesNotExist): with self.assertRaises(models.FederatedUser.DoesNotExist):
models.FederatedUser.objects.get(username="test2") models.FederatedUser.objects.get(username="test2")
def test_json_attributes(self):
"""test the json storage of ``atrributs`` in ``_attributs``"""
provider = models.FederatedIendityProvider.objects.get(suffix="example.com")
user = models.FederatedUser.objects.create(
username=settings.CAS_TEST_USER,
provider=provider,
attributs=settings.CAS_TEST_ATTRIBUTES,
ticket=""
)
self.assertEqual(utils.json_encode(settings.CAS_TEST_ATTRIBUTES), user._attributs)
user.delete()
user = models.FederatedUser.objects.create(
username=settings.CAS_TEST_USER,
provider=provider,
ticket=""
)
self.assertIsNone(user._attributs)
self.assertIsNone(user.attributs)
class FederateSLOTestCase(TestCase, UserModels): class FederateSLOTestCase(TestCase, UserModels):
"""test for the federated SLO model""" """test for the federated SLO model"""
@ -231,3 +252,65 @@ class TicketTestCase(TestCase, UserModels, BaseServicePattern):
self.assertTrue(b'logoutRequest' in params and params[b'logoutRequest']) self.assertTrue(b'logoutRequest' in params and params[b'logoutRequest'])
# only 1 ticket remain in the db # only 1 ticket remain in the db
self.assertEqual(len(models.ServiceTicket.objects.all()), 1) self.assertEqual(len(models.ServiceTicket.objects.all()), 1)
def test_json_attributes(self):
"""test the json storage of ``atrributs`` in ``_attributs``"""
# ge an authenticated client
client = get_auth_client()
# get the user associated to the client
user = self.get_user(client)
ticket = models.ServiceTicket.objects.create(
user=user,
service=self.service,
attributs=settings.CAS_TEST_ATTRIBUTES,
service_pattern=self.service_pattern
)
self.assertEqual(utils.json_encode(settings.CAS_TEST_ATTRIBUTES), ticket._attributs)
ticket.delete()
ticket = models.ServiceTicket.objects.create(
user=user,
service=self.service,
service_pattern=self.service_pattern
)
self.assertIsNone(ticket._attributs)
self.assertIsNone(ticket.attributs)
@mock.patch("cas_server.utils.last_version", lambda: "1.2.3")
@override_settings(ADMINS=[("Ano Nymous", "ano.nymous@example.net")])
@override_settings(CAS_NEW_VERSION_EMAIL_WARNING=True)
class NewVersionWarningTestCase(TestCase):
"""tests for the new version warning model"""
@mock.patch("cas_server.models.VERSION", "0.1.2")
def test_send_mails(self):
"""test the send_mails method with ADMINS and a new version available"""
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(
mail.outbox[0].subject,
'%sA new version of django-cas-server is available' % settings.EMAIL_SUBJECT_PREFIX
)
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 1)
@mock.patch("cas_server.models.VERSION", "1.2.3")
def test_send_mails_same_version(self):
"""test the send_mails method with with current version being the last"""
models.NewVersionWarning.objects.create(version="0.1.2")
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 0)
@override_settings(ADMINS=[])
def test_send_mails_no_admins(self):
"""test the send_mails method without ADMINS"""
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 0)
@override_settings(CAS_NEW_VERSION_EMAIL_WARNING=False)
def test_send_mails_disabled(self):
"""test the send_mails method if disabled"""
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 0)

View File

@ -11,8 +11,11 @@
# (c) 2016 Valentin Samir # (c) 2016 Valentin Samir
"""Tests module for utils""" """Tests module for utils"""
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from django.db import connection
import six import six
import warnings
import datetime
from cas_server import utils from cas_server import utils
@ -128,16 +131,23 @@ class CheckPasswordCase(TestCase):
with self.assertRaises(utils.LdapHashUserPassword.BadHash): with self.assertRaises(utils.LdapHashUserPassword.BadHash):
utils.check_password("ldap", self.password1, b"TOTOssdsdsd", "utf8") utils.check_password("ldap", self.password1, b"TOTOssdsdsd", "utf8")
for scheme in schemes_salt: for scheme in schemes_salt:
# bad length
with self.assertRaises(utils.LdapHashUserPassword.BadHash): with self.assertRaises(utils.LdapHashUserPassword.BadHash):
utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw==", "utf8") utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw==", "utf8")
# bad base64
with self.assertRaises(utils.LdapHashUserPassword.BadHash):
utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw", "utf8")
def test_hex(self): def test_hex(self):
"""test all the hex_HASH method: the hashed password is a simple hash of the password""" """test all the hex_HASH method: the hashed password is a simple hash of the password"""
hashes = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"] hashes = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]
hashed_password1 = [] hashed_password1 = []
for hash in hashes: for hash_scheme in hashes:
hashed_password1.append( hashed_password1.append(
("hex_%s" % hash, getattr(utils.hashlib, hash)(self.password1).hexdigest()) (
"hex_%s" % hash_scheme,
getattr(utils.hashlib, hash_scheme)(self.password1).hexdigest()
)
) )
for (method, hp1) in hashed_password1: for (method, hp1) in hashed_password1:
self.assertTrue(utils.check_password(method, self.password1, hp1, "utf8")) self.assertTrue(utils.check_password(method, self.password1, hp1, "utf8"))
@ -208,3 +218,40 @@ class UtilsTestCase(TestCase):
self.assertEqual(utils.get_tuple(test_tuple, 3), None) self.assertEqual(utils.get_tuple(test_tuple, 3), None)
self.assertEqual(utils.get_tuple(test_tuple, 3, 'toto'), 'toto') self.assertEqual(utils.get_tuple(test_tuple, 3, 'toto'), 'toto')
self.assertEqual(utils.get_tuple(None, 3), None) self.assertEqual(utils.get_tuple(None, 3), None)
def test_last_version(self):
"""
test the function last_version. An internet connection is needed, if you do not have
one, this test will fail and you should ignore it.
"""
try:
# first check if pypi is available
utils.requests.get("https://pypi.python.org/simple/django-cas-server/")
except utils.requests.exceptions.RequestException:
warnings.warn(
(
"Pypi seems not available, perhaps you do not have internet access. "
"Consequently, the test cas_server.tests.test_utils.UtilsTestCase.test_last_"
"version is ignored"
),
RuntimeWarning
)
else:
version = utils.last_version()
self.assertIsInstance(version, six.text_type)
self.assertEqual(len(version.split('.')), 3)
# version is cached 24h so calling it a second time should return the save value
self.assertEqual(version, utils.last_version())
def test_dictfetchall(self):
"""test the function dictfetchall"""
with connection.cursor() as curs:
curs.execute("SELECT * FROM django_migrations")
results = utils.dictfetchall(curs)
self.assertIsInstance(results, list)
self.assertTrue(len(results) > 0)
for result in results:
self.assertIsInstance(result, dict)
self.assertIn('applied', result)
self.assertIsInstance(result['applied'], datetime.datetime)

View File

@ -20,6 +20,7 @@ from django.utils import timezone
import random import random
import json import json
import mock
from lxml import etree from lxml import etree
from six.moves import range from six.moves import range
@ -47,6 +48,33 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
# we prepare a bunch a service url and service patterns for tests # we prepare a bunch a service url and service patterns for tests
self.setup_service_patterns() self.setup_service_patterns()
@override_settings(CAS_NEW_VERSION_HTML_WARNING=True)
@mock.patch("cas_server.utils.last_version", lambda: "1.2.3")
@mock.patch("cas_server.utils.VERSION", "0.1.2")
def test_new_version_available_ok(self):
"""test the new version info box"""
client = Client()
response = client.get("/login")
self.assertIn(b"A new version of the application is available", response.content)
@override_settings(CAS_NEW_VERSION_HTML_WARNING=True)
@mock.patch("cas_server.utils.last_version", lambda: None)
@mock.patch("cas_server.utils.VERSION", "0.1.2")
def test_new_version_available_badpypi(self):
"""
test the new version info box if pypi is not available (unable to retreive last version)
"""
client = Client()
response = client.get("/login")
self.assertNotIn(b"A new version of the application is available", response.content)
@override_settings(CAS_NEW_VERSION_HTML_WARNING=False)
def test_new_version_available_disabled(self):
"""test the new version info box is disabled"""
client = Client()
response = client.get("/login")
self.assertNotIn(b"A new version of the application is available", response.content)
def test_login_view_post_goodpass_goodlt(self): def test_login_view_post_goodpass_goodlt(self):
"""Test a successul login""" """Test a successul login"""
# we get a client who fetch a frist time the login page and the login form default # we get a client who fetch a frist time the login page and the login form default
@ -309,7 +337,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
response = client.get("/login", {'service': service}) response = client.get("/login", {'service': service})
# the ticket is not created and a warning is displayed to the user # the ticket is not created and a warning is displayed to the user
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue(b"User charateristics non allowed" in response.content) self.assertTrue(b"User characteristics non allowed" in response.content)
# same but with rectriction that a valid upon the test user attributes # same but with rectriction that a valid upon the test user attributes
response = client.get("/login", {'service': self.service_filter_success}) response = client.get("/login", {'service': self.service_filter_success})
@ -327,7 +355,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
response = client.get("/login", {'service': self.service_field_needed_fail}) response = client.get("/login", {'service': self.service_field_needed_fail})
# the ticket is not created and a warning is displayed to the user # the ticket is not created and a warning is displayed to the user
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue(b"The attribut uid is needed to use that service" in response.content) self.assertTrue(b"The attribute uid is needed to use that service" in response.content)
# same but with a attribute that the test user has # same but with a attribute that the test user has
response = client.get("/login", {'service': self.service_field_needed_success}) response = client.get("/login", {'service': self.service_field_needed_success})
@ -351,7 +379,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
response = client.get("/login", {"service": self.service_field_needed_success}) response = client.get("/login", {"service": self.service_field_needed_success})
# the ticket is not created and a warning is displayed to the user # the ticket is not created and a warning is displayed to the user
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue(b"The attribut alias is needed to use that service" in response.content) self.assertTrue(b"The attribute alias is needed to use that service" in response.content)
def test_gateway(self): def test_gateway(self):
"""test gateway parameter""" """test gateway parameter"""

View File

@ -12,8 +12,9 @@
"""Some utils functions for tests""" """Some utils functions for tests"""
from cas_server.default_settings import settings from cas_server.default_settings import settings
import django
from django.test import Client from django.test import Client
from django.template import loader, Context from django.template import loader
from django.utils import timezone from django.utils import timezone
import cgi import cgi
@ -21,13 +22,25 @@ import six
from threading import Thread from threading import Thread
from lxml import etree from lxml import etree
from six.moves import BaseHTTPServer from six.moves import BaseHTTPServer
from six.moves.urllib.parse import urlparse, parse_qsl from six.moves.urllib.parse import urlparse, parse_qsl, parse_qs
from datetime import timedelta from datetime import timedelta
from cas_server import models from cas_server import models
from cas_server import utils from cas_server import utils
if django.VERSION < (1, 8):
from django.template import Context
else:
def Context(arg):
"""
Starting from django 1.8 render take a dict and deprecated the use of a Context.
So this is the identity function, only use for compatibility with django 1.7 where
render MUST take a Context as argument.
"""
return arg
def return_unicode(string, charset): def return_unicode(string, charset):
"""make `string` a unicode if `string` is a unicode or bytes encoded with `charset`""" """make `string` a unicode if `string` is a unicode or bytes encoded with `charset`"""
if not isinstance(string, six.text_type): if not isinstance(string, six.text_type):
@ -166,7 +179,7 @@ class HttpParamsHandler(BaseHTTPServer.BaseHTTPRequestHandler):
postvars = cgi.parse_multipart(self.rfile, pdict) postvars = cgi.parse_multipart(self.rfile, pdict)
elif ctype == 'application/x-www-form-urlencoded': elif ctype == 'application/x-www-form-urlencoded':
length = int(self.headers.get('content-length')) length = int(self.headers.get('content-length'))
postvars = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1) postvars = parse_qs(self.rfile.read(length), keep_blank_values=1)
else: else:
postvars = {} postvars = {}
self.server.PARAMS = postvars self.server.PARAMS = postvars

View File

@ -16,8 +16,10 @@ from django.views.decorators.debug import sensitive_post_parameters, sensitive_v
from cas_server import views from cas_server import views
app_name = "cas_server"
urlpatterns = [ urlpatterns = [
url(r'^$', RedirectView.as_view(pattern_name="cas_server:login")), url(r'^$', RedirectView.as_view(pattern_name="cas_server:login", permanent=False)),
url( url(
'^login$', '^login$',
sensitive_post_parameters('password')( sensitive_post_parameters('password')(
@ -51,8 +53,8 @@ urlpatterns = [
url('^samlValidate$', views.SamlValidate.as_view(), name='samlValidate'), url('^samlValidate$', views.SamlValidate.as_view(), name='samlValidate'),
url( url(
'^auth$', '^auth$',
sensitive_variables('password')( sensitive_variables('password', 'secret')(
sensitive_post_parameters('password')( sensitive_post_parameters('password', 'secret')(
views.Auth.as_view() views.Auth.as_view()
) )
), ),

View File

@ -25,11 +25,20 @@ import hashlib
import crypt import crypt
import base64 import base64
import six import six
import requests
import time
import logging
import binascii
from importlib import import_module from importlib import import_module
from datetime import datetime, timedelta from datetime import datetime, timedelta
from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
from . import VERSION
#: logger facility
logger = logging.getLogger(__name__)
def json_encode(obj): def json_encode(obj):
"""Encode a python object to json""" """Encode a python object to json"""
@ -51,6 +60,14 @@ def context(params):
""" """
params["settings"] = settings params["settings"] = settings
params["message_levels"] = DEFAULT_MESSAGE_LEVELS params["message_levels"] = DEFAULT_MESSAGE_LEVELS
if settings.CAS_NEW_VERSION_HTML_WARNING:
LAST_VERSION = last_version()
params["VERSION"] = VERSION
params["LAST_VERSION"] = LAST_VERSION
if LAST_VERSION is not None:
params["upgrade_available"] = decode_version(VERSION) < decode_version(LAST_VERSION)
else:
params["upgrade_available"] = False
return params return params
@ -545,7 +562,10 @@ class LdapHashUserPassword(object):
elif scheme == b'{CRYPT}': elif scheme == b'{CRYPT}':
return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):] return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):]
else: else:
try:
hashed_passord = base64.b64decode(hashed_passord[len(scheme):]) hashed_passord = base64.b64decode(hashed_passord[len(scheme):])
except (TypeError, binascii.Error) as error:
raise cls.BadHash("Bad base64: %s" % error)
if len(hashed_passord) < cls._schemes_to_len[scheme]: if len(hashed_passord) < cls._schemes_to_len[scheme]:
raise cls.BadHash("Hash too short for the scheme %s" % scheme) raise cls.BadHash("Hash too short for the scheme %s" % scheme)
return hashed_passord[cls._schemes_to_len[scheme]:] return hashed_passord[cls._schemes_to_len[scheme]:]
@ -563,7 +583,7 @@ def check_password(method, password, hashed_password, charset):
:param hashed_password: The hashed password as stored in the database :param hashed_password: The hashed password as stored in the database
:type hashed_password: :obj:`str` or :obj:`unicode` :type hashed_password: :obj:`str` or :obj:`unicode`
:param str charset: The used char encoding (also used internally, so it must be valid for :param str charset: The used char encoding (also used internally, so it must be valid for
the charset used by ``password`` even if it is inputed as an :obj:`unicode`) the charset used by ``password`` when it was initially )
:return: True if ``password`` match ``hashed_password`` using ``method``, :return: True if ``password`` match ``hashed_password`` using ``method``,
``False`` otherwise ``False`` otherwise
:rtype: bool :rtype: bool
@ -603,3 +623,60 @@ def check_password(method, password, hashed_password, charset):
)(password).hexdigest().encode("ascii") == hashed_password.lower() )(password).hexdigest().encode("ascii") == hashed_password.lower()
else: else:
raise ValueError("Unknown password method check %r" % method) raise ValueError("Unknown password method check %r" % method)
def decode_version(version):
"""
decode a version string following version semantic http://semver.org/ input a tuple of int
:param unicode version: A dotted version
:return: A tuple a int
:rtype: tuple
"""
return tuple(int(sub_version) for sub_version in version.split('.'))
def last_version():
"""
Fetch the last version from pypi and return it. On successful fetch from pypi, the response
is cached 24h, on error, it is cached 10 min.
:return: the last django-cas-server version
:rtype: unicode
"""
try:
last_update, version, success = last_version._cache
except AttributeError:
last_update = 0
version = None
success = False
cache_delta = 24 * 3600 if success else 600
if (time.time() - last_update) < cache_delta:
return version
else:
try:
req = requests.get(settings.CAS_NEW_VERSION_JSON_URL)
data = json.loads(req.text)
versions = list(data["releases"].keys())
versions.sort()
version = versions[-1]
last_version._cache = (time.time(), version, True)
return version
except (
KeyError,
ValueError,
requests.exceptions.RequestException
) as error: # pragma: no cover (should not happen unless pypi is not available)
logger.error(
"Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error)
)
last_version._cache = (time.time(), version, False)
def dictfetchall(cursor):
"Return all rows from a django cursor as a dict"
columns = [col[0] for col in cursor.description]
return [
dict(zip(columns, row))
for row in cursor.fetchall()
]

View File

@ -147,9 +147,12 @@ class LogoutView(View, LogoutMixin):
# current querystring # current querystring
if settings.CAS_FEDERATE: if settings.CAS_FEDERATE:
if auth is not None: if auth is not None:
params = utils.copy_params(request.GET) params = utils.copy_params(request.GET, ignore={"forget_provider"})
url = auth.get_logout_url() url = auth.get_logout_url()
return HttpResponseRedirect(utils.update_url(url, params)) response = HttpResponseRedirect(utils.update_url(url, params))
if request.GET.get("forget_provider"):
response.delete_cookie("remember_provider")
return response
# if service is set, redirect to service after logout # if service is set, redirect to service after logout
if self.service: if self.service:
list(messages.get_messages(request)) # clean messages before leaving the django app list(messages.get_messages(request)) # clean messages before leaving the django app
@ -209,6 +212,7 @@ class LogoutView(View, LogoutMixin):
class FederateAuth(View): class FederateAuth(View):
"""view to authenticated user agains a backend CAS then CAS_FEDERATE is True""" """view to authenticated user agains a backend CAS then CAS_FEDERATE is True"""
@method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception @method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
""" """
@ -218,8 +222,7 @@ class FederateAuth(View):
""" """
return super(FederateAuth, self).dispatch(request, *args, **kwargs) return super(FederateAuth, self).dispatch(request, *args, **kwargs)
@staticmethod def get_cas_client(self, request, provider, renew=False):
def get_cas_client(request, provider):
""" """
return a CAS client object matching provider return a CAS client object matching provider
@ -231,7 +234,8 @@ class FederateAuth(View):
""" """
# compute the current url, ignoring ticket dans provider GET parameters # compute the current url, ignoring ticket dans provider GET parameters
service_url = utils.get_current_url(request, {"ticket", "provider"}) service_url = utils.get_current_url(request, {"ticket", "provider"})
return CASFederateValidateUser(provider, service_url) self.service_url = service_url
return CASFederateValidateUser(provider, service_url, renew=renew)
def post(self, request, provider=None): def post(self, request, provider=None):
""" """
@ -264,24 +268,16 @@ class FederateAuth(View):
if form.is_valid(): if form.is_valid():
params = utils.copy_params( params = utils.copy_params(
request.POST, request.POST,
ignore={"provider", "csrfmiddlewaretoken", "ticket"} ignore={"provider", "csrfmiddlewaretoken", "ticket", "lt"}
) )
if params.get("renew") == "False":
del params["renew"]
url = utils.reverse_params( url = utils.reverse_params(
"cas_server:federateAuth", "cas_server:federateAuth",
kwargs=dict(provider=form.cleaned_data["provider"].suffix), kwargs=dict(provider=form.cleaned_data["provider"].suffix),
params=params params=params
) )
response = HttpResponseRedirect(url) return HttpResponseRedirect(url)
# If the user has checked "remember my identity provider" store it in a cookie
if form.cleaned_data["remember"]:
max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT
utils.set_cookie(
response,
"_remember_provider",
form.cleaned_data["provider"].suffix,
max_age
)
return response
else: else:
return redirect("cas_server:login") return redirect("cas_server:login")
@ -296,22 +292,24 @@ class FederateAuth(View):
if not settings.CAS_FEDERATE: if not settings.CAS_FEDERATE:
logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode") logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode")
return redirect("cas_server:login") return redirect("cas_server:login")
renew = bool(request.GET.get('renew') and request.GET['renew'] != "False")
# Is the user is already authenticated, no need to request authentication to the user # Is the user is already authenticated, no need to request authentication to the user
# identity provider. # identity provider.
if self.request.session.get("authenticated"): if self.request.session.get("authenticated") and not renew:
logger.warning("User already authenticated, dropping federate authentication request") logger.warning("User already authenticated, dropping federate authentication request")
return redirect("cas_server:login") return redirect("cas_server:login")
try: try:
# get the identity provider from its suffix # get the identity provider from its suffix
provider = FederatedIendityProvider.objects.get(suffix=provider) provider = FederatedIendityProvider.objects.get(suffix=provider)
# get a CAS client for the user identity provider # get a CAS client for the user identity provider
auth = self.get_cas_client(request, provider) auth = self.get_cas_client(request, provider, renew)
# if no ticket submited, redirect to the identity provider CAS login page # if no ticket submited, redirect to the identity provider CAS login page
if 'ticket' not in request.GET: if 'ticket' not in request.GET:
logger.info("Trying to authenticate again %s" % auth.provider.server_url) logger.info("Trying to authenticate again %s" % auth.provider.server_url)
return HttpResponseRedirect(auth.get_login_url()) return HttpResponseRedirect(auth.get_login_url())
else: else:
ticket = request.GET['ticket'] ticket = request.GET['ticket']
try:
# if the ticket validation succeed # if the ticket validation succeed
if auth.verify_ticket(ticket): if auth.verify_ticket(ticket):
logger.info( logger.info(
@ -320,23 +318,55 @@ class FederateAuth(View):
auth.provider.server_url auth.provider.server_url
) )
) )
params = utils.copy_params(request.GET, ignore={"ticket"}) params = utils.copy_params(request.GET, ignore={"ticket", "remember"})
request.session["federate_username"] = auth.federated_username request.session["federate_username"] = auth.federated_username
request.session["federate_ticket"] = ticket request.session["federate_ticket"] = ticket
auth.register_slo(auth.federated_username, request.session.session_key, ticket) auth.register_slo(
auth.federated_username,
request.session.session_key,
ticket
)
# redirect to the the login page for the user to become authenticated # redirect to the the login page for the user to become authenticated
# thanks to the `federate_username` and `federate_ticket` session parameters # thanks to the `federate_username` and `federate_ticket` session parameters
url = utils.reverse_params("cas_server:login", params) url = utils.reverse_params("cas_server:login", params)
return HttpResponseRedirect(url) response = HttpResponseRedirect(url)
# If the user has checked "remember my identity provider" store it in a
# cookie
if request.GET.get("remember"):
max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT
utils.set_cookie(
response,
"remember_provider",
provider.suffix,
max_age
)
return response
# else redirect to the identity provider CAS login page # else redirect to the identity provider CAS login page
else: else:
logger.info( logger.info(
"Got a invalid ticket for %s from %s. Retrying to authenticate" % ( (
auth.username, "Got a invalid ticket %s from %s for service %s. "
auth.provider.server_url "Retrying to authenticate"
) % (
ticket,
auth.provider.server_url,
self.service_url
) )
) )
return HttpResponseRedirect(auth.get_login_url()) return HttpResponseRedirect(auth.get_login_url())
# both xml.etree.ElementTree and lxml.etree exceptions inherit from SyntaxError
except SyntaxError as error:
messages.add_message(
request,
messages.ERROR,
_(
u"Invalid response from your identity provider CAS upon "
u"ticket %(ticket)s validation: %(error)r"
) % {'ticket': ticket, 'error': error}
)
response = redirect("cas_server:login")
response.delete_cookie("remember_provider")
return response
except FederatedIendityProvider.DoesNotExist: except FederatedIendityProvider.DoesNotExist:
logger.warning("Identity provider suffix %s not found" % provider) logger.warning("Identity provider suffix %s not found" % provider)
# if the identity provider is not found, redirect to the login page # if the identity provider is not found, redirect to the login page
@ -407,7 +437,8 @@ class LoginView(View, LogoutMixin):
self.warn = request.POST.get('warn') self.warn = request.POST.get('warn')
if settings.CAS_FEDERATE: if settings.CAS_FEDERATE:
self.username = request.POST.get('username') self.username = request.POST.get('username')
self.ticket = request.POST.get('ticket') # in federated mode, the valdated indentity provider CAS ticket is used as password
self.ticket = request.POST.get('password')
def gen_lt(self): def gen_lt(self):
"""Generate a new LoginTicket and add it to the list of valid LT for the user""" """Generate a new LoginTicket and add it to the list of valid LT for the user"""
@ -451,7 +482,7 @@ class LoginView(View, LogoutMixin):
messages.add_message( messages.add_message(
self.request, self.request,
messages.ERROR, messages.ERROR,
_(u"Invalid login ticket") _(u"Invalid login ticket, please retry to login")
) )
elif ret == self.USER_LOGIN_OK: elif ret == self.USER_LOGIN_OK:
# On successful login, update the :class:`models.User<cas_server.models.User>` ``date`` # On successful login, update the :class:`models.User<cas_server.models.User>` ``date``
@ -477,7 +508,17 @@ class LoginView(View, LogoutMixin):
else: # pragma: no cover (should no happen) else: # pragma: no cover (should no happen)
raise EnvironmentError("invalid output for LoginView.process_post") raise EnvironmentError("invalid output for LoginView.process_post")
# call the GET/POST common part # call the GET/POST common part
return self.common() response = self.common()
if self.warn:
utils.set_cookie(
response,
"warn",
"on",
10 * 365 * 24 * 3600
)
else:
response.delete_cookie("warn")
return response
def process_post(self): def process_post(self):
""" """
@ -586,7 +627,9 @@ class LoginView(View, LogoutMixin):
form_initial = { form_initial = {
'service': self.service, 'service': self.service,
'method': self.method, 'method': self.method,
'warn': self.warn or self.request.session.get("warn"), 'warn': (
self.warn or self.request.session.get("warn") or self.request.COOKIES.get('warn')
),
'lt': self.request.session['lt'][-1], 'lt': self.request.session['lt'][-1],
'renew': self.renew 'renew': self.renew
} }
@ -683,14 +726,14 @@ class LoginView(View, LogoutMixin):
messages.add_message( messages.add_message(
self.request, self.request,
messages.ERROR, messages.ERROR,
_(u"User charateristics non allowed") _(u"User characteristics non allowed")
) )
except models.UserFieldNotDefined: except models.UserFieldNotDefined:
error = 4 error = 4
messages.add_message( messages.add_message(
self.request, self.request,
messages.ERROR, messages.ERROR,
_(u"The attribut %(field)s is needed to use" _(u"The attribute %(field)s is needed to use"
u" that service") % {'field': service_pattern.user_field} u" that service") % {'field': service_pattern.user_field}
) )
@ -817,19 +860,37 @@ class LoginView(View, LogoutMixin):
) )
else: else:
if ( if (
self.request.COOKIES.get('_remember_provider') and self.request.COOKIES.get('remember_provider') and
FederatedIendityProvider.objects.filter( FederatedIendityProvider.objects.filter(
suffix=self.request.COOKIES['_remember_provider'] suffix=self.request.COOKIES['remember_provider']
) )
): ):
params = utils.copy_params(self.request.GET) params = utils.copy_params(self.request.GET)
url = utils.reverse_params( url = utils.reverse_params(
"cas_server:federateAuth", "cas_server:federateAuth",
params=params, params=params,
kwargs=dict(provider=self.request.COOKIES['_remember_provider']) kwargs=dict(provider=self.request.COOKIES['remember_provider'])
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
# if user is authenticated and auth renewal is requested, redirect directly
# to the user identity provider
if self.renew and self.request.session.get("authenticated"):
try:
user = FederatedUser.get_from_federated_username(
self.request.session.get("username")
)
params = utils.copy_params(self.request.GET)
url = utils.reverse_params(
"cas_server:federateAuth",
params=params,
kwargs=dict(provider=user.provider.suffix)
)
return HttpResponseRedirect(url)
# Should normally not happen: if the user is logged, it exists in the
# database.
except FederatedUser.DoesNotExist: # pragma: no cover
pass
return render( return render(
self.request, self.request,
settings.CAS_LOGIN_TEMPLATE, settings.CAS_LOGIN_TEMPLATE,

View File

@ -1,10 +1,12 @@
setuptools>=5.5 setuptools>=5.5
requests>=2.4
requests_futures>=0.9.5
lxml>=3.4
six>=1.8
tox>=1.8.1 tox>=1.8.1
pytest>=2.6.4 pytest>=2.6.4
pytest-django>=2.8.0 pytest-django>=2.8.0
pytest-pythonpath>=0.3 pytest-pythonpath>=0.3
pytest-warnings
pytest-cov>=2.2.1 pytest-cov>=2.2.1
requests>=2.4 mock>=1
requests_futures>=0.9.5
lxml>=3.4
six>=1

View File

@ -1,6 +1,6 @@
Django >= 1.8,<1.10 Django >= 1.7.1,<1.10
setuptools>=5.5 setuptools>=5.5
requests>=2.4 requests>=2.4
requests_futures>=0.9.5 requests_futures>=0.9.5
lxml>=3.4 lxml>=3.4
six>=1 six>=1.8

View File

@ -1,8 +1,7 @@
import os import os
import pkg_resources import pkg_resources
from setuptools import setup from setuptools import setup
from cas_server import VERSION
VERSION = '0.6.1'
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
README = readme.read() README = readme.read()
@ -30,7 +29,7 @@ if __name__ == '__main__':
author_email='valentin.samir@crans.org', author_email='valentin.samir@crans.org',
classifiers=[ classifiers=[
'Environment :: Web Environment', 'Environment :: Web Environment',
'evelopment Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Framework :: Django', 'Framework :: Django',
'Framework :: Django :: 1.7', 'Framework :: Django :: 1.7',
'Framework :: Django :: 1.8', 'Framework :: Django :: 1.8',
@ -66,5 +65,5 @@ if __name__ == '__main__':
download_url="https://github.com/nitmir/django-cas-server/releases", download_url="https://github.com/nitmir/django-cas-server/releases",
zip_safe=False, zip_safe=False,
setup_requires=['pytest-runner'], setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-django', 'pytest-pythonpath'], tests_require=['pytest', 'pytest-django', 'pytest-pythonpath', 'pytest-warnings', 'mock>=1'],
) )

View File

@ -31,7 +31,7 @@ whitelist_externals=
[testenv] [testenv]
commands= commands=
py.test {posargs:cas_server/tests/} py.test -rw {posargs:cas_server/tests/}
{[post_cmd]commands} {[post_cmd]commands}
whitelist_externals={[post_cmd]whitelist_externals} whitelist_externals={[post_cmd]whitelist_externals}