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
matrix:
include:
- python: "2.7"
env: TOX_ENV=coverage
- python: "2.7"
env: TOX_ENV=flake8
- python: "2.7"
@ -23,6 +21,8 @@ matrix:
env: TOX_ENV=py35-django18
- python: "3.5"
env: TOX_ENV=py35-django19
- python: "2.7"
env: TOX_ENV=coverage
cache:
directories:
- $HOME/.cache/pip/http/

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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