...
 
Commits (57)
......@@ -6,27 +6,19 @@ matrix:
- python: "2.7"
env: TOX_ENV=check_rst
- python: "2.7"
env: TOX_ENV=py27-django17
- python: "2.7"
env: TOX_ENV=py27-django18
- python: "2.7"
env: TOX_ENV=py27-django19
- python: "2.7"
env: TOX_ENV=py27-django110
- python: "3.4"
env: TOX_ENV=py34-django17
- python: "3.4"
env: TOX_ENV=py34-django18
- python: "3.4"
env: TOX_ENV=py34-django19
- python: "3.4"
env: TOX_ENV=py34-django110
env: TOX_ENV=py27-django111
- python: "3.5"
env: TOX_ENV=py35-django18
env: TOX_ENV=py35-django111
- python: "3.6"
env: TOX_ENV=py36-django111
- python: "3.5"
env: TOX_ENV=py35-django19
env: TOX_ENV=py35-django20
- python: "3.6"
env: TOX_ENV=py36-django20
- python: "3.5"
env: TOX_ENV=py35-django110
env: TOX_ENV=py35-django21
- python: "3.6"
env: TOX_ENV=py36-django21
- python: "2.7"
env: TOX_ENV=coverage
cache:
......
......@@ -2,24 +2,25 @@
BASEDIR="$1"
PROJECT_NAME="$2"
cd "$BASEDIR/htmlcov/"; tar czf "$BASEDIR/coverage.tar.gz" ./
cd "$BASEDIR"
TITLE="Coverage report of $PROJECT_NAME"
# build by gitlab CI
if [ -n "$CI_BUILD_REF_NAME" ]; then
BRANCH="$CI_BUILD_REF_NAME"
TITLE="$TITLE, $BRANCH branch"
# build by travis
elif [ -n "$TRAVIS_BRANCH" ]; then
# if this a pull request ?
if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then
BRANCH="pull-request-$TRAVIS_PULL_REQUEST"
TITLE="$TITLE, pull request n°$BRANCH"
else
BRANCH="$TRAVIS_BRANCH"
TITLE="$TITLE, $BRANCH branch"
fi
else
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
TITLE="$TITLE, $BRANCH branch"
fi
if [[ "$BRANCH" = "HEAD" ]] || [ -z "$BRANCH" ]; then
......@@ -27,7 +28,23 @@ if [[ "$BRANCH" = "HEAD" ]] || [ -z "$BRANCH" ]; then
exit 0
fi
curl https://badges.genua.fr/local/coverage/ \
VENV="$(mktemp -d)"
HTMLREPORT="$(mktemp -d)"
virtualenv "$VENV"
"$VENV/bin/pip" install coverage
"$VENV/bin/coverage" html --title "$TITLE" --directory "$HTMLREPORT"
rm -rf "$VENV"
cd "$HTMLREPORT"; tar czf "$BASEDIR/coverage.tar.gz" ./
cd "$BASEDIR"
rm -rf "$HTMLREPORT"
curl https://badges.genua.fr/coverage/ \
-F "secret=$COVERAGE_TOKEN" \
-F "tar=@$BASEDIR/coverage.tar.gz" \
-F "project=$PROJECT_NAME" \
......
......@@ -6,6 +6,98 @@ All notable changes to this project will be documented in this file.
.. contents:: Table of Contents
:depth: 2
v1.1.0 - 2019-03-02
===================
Added
-----
* Support for Django 2.1
Fixes
-----
* Checkbox position on the login page
* Set ldap3 client_strategy from sync to sync-restartable
* Deprecation warning for {% load staticfiles %} and django.contrib.staticfiles
v1.0.0 - 2019-01-12
===================
Added
-----
* Support for python 3.6 and Django 1.11
* Support for Django 2.0
* Keep query string then redirecting from / to /login
Fixes
-----
* Add missing attributes authenticationDate, longTermAuthenticationRequestTokenUsed and
isFromNewLogin from service validation response
* Catch error from calling django.contrib.staticfiles.templatetags.staticfiles.static
in non-debug mode before collectstatic in cas_server.default_settings.py
* Invalid escape sequence in regular expression
Deprecated
----------
* Support for Django <1.11 is dropped, it should still works for this version.
Next versions will most probably be not compatible with Django <1.11
* Support for python 3.4 is dropped, it should still works for this version.
Next versions may or may not works with python 3.4.
Other
-----
* Migrations have been squashed for Django 2.0 support. Be sur to apply all migration before
updating to this version
* Update PyPi url from https://pypi.python.org to https://pypi.org
v0.9.0 - 2017-11-17
===================
Added
-----
* Dutch translation
* Protuguese translation (brazilian variant)
* Support for ldap3 version 2 or more (changes in the API)
All exception are now in ldap3.core.exceptions, methodes for fetching attritutes and
dn are renamed.
* Possibility to disable service message boxes on the login pages
Fixed
-----
* Then using the LDAP auth backend with ``bind`` method for password check, do not try to bind
if the user dn was not found. This was causing the exception
``'NoneType' object has no attribute 'getitem'`` describe in #21
* Increase the max size of usernames (30 chars to 250)
* Fix XSS js injection
v0.8.0 - 2017-03-08
===================
Added
-----
* Add a test for login with missing parameter (username or password or both)
* Add ldap auth using bind method (use the user credentials to bind the the ldap server and let the
server check the credentials)
* Add CAS_TGT_VALIDITY parameter: Max time after with the user MUST reauthenticate.
Fixed
-----
* Allow both unicode and bytes dotted string in utils.import_attr
* Fix some spelling and grammar on log messages. (thanks to Allie Micka)
* Fix froms css class error on success/error due to a scpaless block
* Disable pip cache then installing with make install
Changed
-------
* Update french translation
v0.7.4 - 2016-09-07
===================
......
......@@ -6,7 +6,7 @@ build:
install: dist
pip -V
pip install --no-deps --upgrade --force-reinstall --find-links ./dist/django-cas-server-${VERSION}.tar.gz django-cas-server
pip install --no-cache-dir --no-deps --upgrade --force-reinstall --find-links ./dist/django-cas-server-${VERSION}.tar.gz django-cas-server
uninstall:
pip uninstall django-cas-server || true
......@@ -37,8 +37,8 @@ dist:
python setup.py sdist
test_venv/bin/python:
virtualenv test_venv
test_venv/bin/pip install -U --requirement requirements-dev.txt 'Django<1.11'
python3 -m venv test_venv
test_venv/bin/pip install -U --requirement requirements-dev.txt 'Django>=2.0,<2.1'
test_venv/cas/manage.py: test_venv
mkdir -p test_venv/cas
......
......@@ -21,15 +21,15 @@ Features
* Possibility to rename/rewrite attributes per service
* Possibility to require some attribute values per service
* Federated mode between multiple CAS
* Supports Django 1.7, 1.8 and 1.9
* Supports Python 2.7, 3.x
* Supports Django 1.11 and 2.0
* Supports Python 2.7, 3.5+
Dependencies
============
``django-cas-server`` depends on the following python packages:
* Django >= 1.7.1 < 1.11
* Django >= 1.11 < 2.1
* requests >= 2.4
* requests_futures >= 0.9.5
* lxml >= 3.4
......@@ -218,7 +218,8 @@ Template settings
}
if you omit some keys of the dictionnary, the default value for these keys is used.
* ``CAS_SHOW_SERVICE_MESSAGES``: Messages displayed about the state of the service on the login page.
The default is ``True``.
* ``CAS_INFO_MESSAGES``: Messages displayed in info-boxes on the html pages of the default templates.
It is a dictionnary mapping message name to a message dict. A message dict has 3 keys:
......@@ -268,6 +269,11 @@ Authentication settings
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_TGT_VALIDITY``: Max time after with the user MUST reauthenticate. Let it to `None` for no
max time.This can be used to force refreshing cached informations only available upon user
authentication like the user attributes in federation mode or with the ldap auth in bind mode.
The default is ``None``.
* ``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
tell requests to use its internal certificat authorities. Settings it to ``False`` should
......@@ -416,6 +422,14 @@ Only usefull if you are using the ldap authentication backend:
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.
* ``"bind``, the user credentials are used to bind to the ldap database and retreive the user
attribute. In this mode, the settings ``CAS_LDAP_PASSWORD_ATTR`` and ``CAS_LDAP_PASSWORD_CHARSET``
are ignored, and it is the ldap server that perform password check. The counterpart is that
the user attributes are only available upon user password check and so are cached for later
use. All the other modes directly fetch the user attributes from the database whenever there
are needed. This mean that is you use this mode, they can be some difference between the
attributes in database and the cached ones if changes happend in the database after the user
authentiate. See the parameter ``CAS_TGT_VALIDITY`` to force user to reauthenticate periodically.
The default is ``"ldap"``.
* ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to
......@@ -585,6 +599,10 @@ to the provider CAS to authenticate. This provider transmit to ``django-cas-serv
username and attributes. The user is now logged in on ``django-cas-server`` and can use
services using ``django-cas-server`` as CAS.
In federation mode, the user attributes are cached upon user authentication. See the settings
``CAS_TGT_VALIDITY`` to force users to reauthenticate periodically and allow ``django-cas-server``
to refresh cached attributes.
The list of allowed identity providers is defined using the django admin application.
With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers.
......@@ -627,7 +645,7 @@ You could for example do as bellow::
:target: https://travis-ci.org/nitmir/django-cas-server
.. |pypi_version| image:: https://badges.genua.fr/pypi/v/django-cas-server.svg
:target: https://pypi.python.org/pypi/django-cas-server
:target: https://pypi.org/project/django-cas-server/
.. |github_version| image:: https://badges.genua.fr/github/tag/nitmir/django-cas-server.svg?label=github
:target: https://github.com/nitmir/django-cas-server/releases/latest
......@@ -638,8 +656,8 @@ You could for example do as bellow::
.. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg
:target: https://www.codacy.com/app/valentin-samir/django-cas-server
.. |coverage| image:: https://badges.genua.fr/local/coverage/?project=django-cas-server&branch=master
:target: https://badges.genua.fr/local/coverage/django-cas-server/master
.. |coverage| image:: https://intranet.genua.fr/coverage/badge/django-cas-server/master.svg
:target: https://badges.genua.fr/coverage/django-cas-server/master
.. |doc| image:: https://badges.genua.fr/local/readthedocs/?version=latest
:target: http://django-cas-server.readthedocs.io
......@@ -11,7 +11,7 @@
"""A django CAS server application"""
#: version of the application
VERSION = '0.7.4'
VERSION = '1.1.0'
#: path the the application configuration class
default_app_config = 'cas_server.apps.CasAppConfig'
......@@ -9,10 +9,12 @@
#
# (c) 2015-2016 Valentin Samir
"""module for the admin interface of the app"""
from .default_settings import settings
from django.contrib import admin
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern
from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterAttributValue
from .models import FederatedIendityProvider
from .models import FederatedIendityProvider, FederatedUser, UserAttributes
from .forms import TicketForm
......@@ -167,6 +169,33 @@ class FederatedIendityProviderAdmin(admin.ModelAdmin):
list_display = ('verbose_name', 'suffix', 'display')
admin.site.register(User, UserAdmin)
class FederatedUserAdmin(admin.ModelAdmin):
"""
Bases: :class:`django.contrib.admin.ModelAdmin`
:class:`FederatedUser<cas_server.models.FederatedUser>` in admin
interface
"""
#: Fields to display on a object.
fields = ('username', 'provider', 'last_update')
#: Fields to display on the list of class:`FederatedUserAdmin` objects.
list_display = ('username', 'provider', 'last_update')
class UserAttributesAdmin(admin.ModelAdmin):
"""
Bases: :class:`django.contrib.admin.ModelAdmin`
:class:`UserAttributes<cas_server.models.UserAttributes>` in admin
interface
"""
#: Fields to display on a object.
fields = ('username', '_attributs')
admin.site.register(ServicePattern, ServicePatternAdmin)
admin.site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)
if settings.DEBUG: # pragma: no branch (we always test with DEBUG True)
admin.site.register(User, UserAdmin)
admin.site.register(FederatedUser, FederatedUserAdmin)
admin.site.register(UserAttributes, UserAttributesAdmin)
......@@ -27,10 +27,11 @@ except ImportError:
try: # pragma: no cover
import ldap3
import ldap3.core.exceptions
except ImportError:
ldap3 = None
from .models import FederatedUser
from .models import FederatedUser, UserAttributes
from .utils import check_password, dictfetchall
......@@ -49,7 +50,7 @@ class AuthUser(object):
def test_password(self, password):
"""
Tests ``password`` agains the user password.
Tests ``password`` against the user-supplied password.
:raises NotImplementedError: always. The method need to be implemented by subclasses
"""
......@@ -74,7 +75,7 @@ class DummyAuthUser(AuthUser): # pragma: no cover
def test_password(self, password):
"""
Tests ``password`` agains the user password.
Tests ``password`` against the user-supplied password.
:param unicode password: a clear text password as submited by the user.
:return: always ``False``
......@@ -102,7 +103,7 @@ class TestAuthUser(AuthUser):
def test_password(self, password):
"""
Tests ``password`` agains the user password.
Tests ``password`` against the user-supplied password.
:param unicode password: a clear text password as submited by the user.
:return: ``True`` if :attr:`username<AuthUser.username>` is valid and
......@@ -149,7 +150,7 @@ class MysqlAuthUser(DBAuthUser): # pragma: no cover
"""
DEPRECATED, use :class:`SqlAuthUser` instead.
A mysql authentication class: authenticate user agains a mysql database
A mysql authentication class: authenticate user against 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
......@@ -188,7 +189,7 @@ class MysqlAuthUser(DBAuthUser): # pragma: no cover
def test_password(self, password):
"""
Tests ``password`` agains the user password.
Tests ``password`` against the user-supplied 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
......@@ -208,7 +209,7 @@ class MysqlAuthUser(DBAuthUser): # pragma: no cover
class SqlAuthUser(DBAuthUser): # pragma: no cover
"""
A SQL authentication class: authenticate user agains a SQL database. The SQL database
A SQL authentication class: authenticate user against 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>`
......@@ -238,7 +239,7 @@ class SqlAuthUser(DBAuthUser): # pragma: no cover
def test_password(self, password):
"""
Tests ``password`` agains the user password.
Tests ``password`` against the user-supplied 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
......@@ -276,6 +277,7 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
settings.CAS_LDAP_SERVER,
settings.CAS_LDAP_USER,
settings.CAS_LDAP_PASSWORD,
client_strategy="RESTARTABLE",
auto_bind=True
)
cls._conn = conn
......@@ -284,6 +286,10 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
def __init__(self, username):
if not ldap3:
raise RuntimeError("Please install ldap3 before using the LdapAuthUser backend")
if not settings.CAS_LDAP_BASE_DN:
raise ValueError(
"You must define CAS_LDAP_BASE_DN for using the ldap authentication backend"
)
# in case we got deconnected from the database, retry to connect 2 times
for retry_nb in range(3):
try:
......@@ -293,7 +299,19 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
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()
# try the new ldap3>=2 API
try:
user = conn.entries[0].entry_attributes_as_dict
# store the user dn
user["dn"] = conn.entries[0].entry_dn
# fallback to ldap3<2 API
except (
ldap3.core.exceptions.LDAPKeyError, # ldap3<1 exception
ldap3.core.exceptions.LDAPAttributeError # ldap3<2 exception
):
user = conn.entries[0].entry_get_attributes_dict()
# store the user dn
user["dn"] = conn.entries[0].entry_get_dn()
if user.get(settings.CAS_LDAP_USERNAME_ATTR):
self.user = user
super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0])
......@@ -302,20 +320,60 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
else:
super(LdapAuthUser, self).__init__(username)
break
except ldap3.LDAPCommunicationError:
except ldap3.core.exceptions.LDAPCommunicationError:
if retry_nb == 2:
raise
def test_password(self, password):
"""
Tests ``password`` agains the user password.
Tests ``password`` against the user-supplied 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):
if self.user and settings.CAS_LDAP_PASSWORD_CHECK == "bind":
try:
conn = ldap3.Connection(
settings.CAS_LDAP_SERVER,
self.user["dn"],
password,
auto_bind=True
)
try:
# fetch the user attribute
if conn.search(
settings.CAS_LDAP_BASE_DN,
settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(self.username),
attributes=ldap3.ALL_ATTRIBUTES
) and len(conn.entries) == 1:
# try the ldap3>=2 API
try:
attributes = conn.entries[0].entry_attributes_as_dict
# store the user dn
attributes["dn"] = conn.entries[0].entry_dn
# fallback to ldap<2 API
except (
ldap3.core.exceptions.LDAPKeyError, # ldap3<1 exception
ldap3.core.exceptions.LDAPAttributeError # ldap3<2 exception
):
attributes = conn.entries[0].entry_get_attributes_dict()
attributes["dn"] = conn.entries[0].entry_get_dn()
# cache the attributes locally as we wont have access to the user password
# later.
user = UserAttributes.objects.get_or_create(username=self.username)[0]
user.attributs = attributes
user.save()
finally:
conn.unbind()
return True
except (
ldap3.core.exceptions.LDAPBindError,
ldap3.core.exceptions.LDAPCommunicationError
):
return False
elif self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR):
return check_password(
settings.CAS_LDAP_PASSWORD_CHECK,
password,
......@@ -325,6 +383,22 @@ class LdapAuthUser(DBAuthUser): # 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
:raises NotImplementedError: if the password check method in `CAS_LDAP_PASSWORD_CHECK`
do not allow to fetch the attributes without the user credentials.
"""
if settings.CAS_LDAP_PASSWORD_CHECK == "bind":
raise NotImplementedError()
else:
return super(LdapAuthUser, self).attributs()
class DjangoAuthUser(AuthUser): # pragma: no cover
"""
......@@ -347,7 +421,7 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
def test_password(self, password):
"""
Tests ``password`` agains the user password.
Tests ``password`` against the user-supplied password.
:param unicode password: a clear text password as submited by the user.
:return: ``True`` if :attr:`user` is valid and ``password`` is
......@@ -426,7 +500,7 @@ class CASFederateAuth(AuthUser):
def test_password(self, ticket):
"""
Tests ``password`` agains the user password.
Tests ``password`` against the user-supplied password.
:param unicode password: The CAS tickets just used to validate the user authentication
against its CAS backend.
......
......@@ -206,7 +206,7 @@ class CASClientV2(CASClientBase, ReturnUnicode):
def parse_attributes_xml_element(cls, element, charset):
attributes = dict()
for attribute in element:
tag = cls.self.u(attribute.tag, charset).split(u"}").pop()
tag = cls.u(attribute.tag, charset).split(u"}").pop()
if tag in attributes:
if isinstance(attributes[tag], list):
attributes[tag].append(cls.u(attribute.text, charset))
......
......@@ -11,16 +11,26 @@
# (c) 2015-2016 Valentin Samir
"""Default values for the app's settings"""
from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.templatetags.static import static
from django.utils.translation import ugettext_lazy as _
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")
try:
#: 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")
# is settings.DEBUG is False and collectstatics has not been run yet, the static function will
# raise a ValueError because the file is not found.
except ValueError:
#: URL to the logo showed in the up left corner on the default templates.
CAS_LOGO_URL = None
#: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon.
CAS_FAVICON_URL = None
#: Show the powered by footer if set to ``True``
CAS_SHOW_POWERED = True
#: URLs to css and javascript external components.
......@@ -58,6 +68,10 @@ CAS_SLO_MAX_PARALLEL_REQUESTS = 10
CAS_SLO_TIMEOUT = 5
#: Shared to transmit then using the view :class:`cas_server.views.Auth`
CAS_AUTH_SHARED_SECRET = ''
#: Max time after with the user MUST reauthenticate. Let it to `None` for no max time.
#: This can be used to force refreshing cached informations only available upon user authentication
#: like the user attributes in federation mode or with the ldap auth in bind mode.
CAS_TGT_VALIDITY = None
#: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time
......@@ -179,8 +193,10 @@ CAS_NEW_VERSION_HTML_WARNING = True
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"
CAS_NEW_VERSION_JSON_URL = "https://pypi.org/pypi/django-cas-server/json"
#: If the service message should be displayed on the login page
CAS_SHOW_SERVICE_MESSAGES = True
#: Messages displayed in a info-box on the html pages of the default templates.
#: ``CAS_INFO_MESSAGES`` is a :class:`dict` mapping message name to a message :class:`dict`.
......
......@@ -69,7 +69,7 @@ class CASFederateValidateUser(object):
def verify_ticket(self, ticket):
"""
test ``ticket`` agains the CAS provider, if valid, create a
test ``ticket`` against the CAS provider, if valid, create a
:class:`FederatedUser<cas_server.models.FederatedUser>` matching provider returned
username and attributes.
......
......@@ -31,7 +31,10 @@ class BootsrapForm(forms.Form):
# Only tweak the field if it will be displayed
if not isinstance(field.widget, widgets.HiddenInput):
attrs = {}
if isinstance(field.widget, (widgets.Input, widgets.Select, widgets.Textarea)):
if (
isinstance(field.widget, (widgets.Input, widgets.Select, widgets.Textarea)) and
not isinstance(field.widget, (widgets.CheckboxInput,))
):
attrs['class'] = "form-control"
if isinstance(field.widget, (widgets.Input, widgets.Textarea)) and field.label:
attrs["placeholder"] = field.label
......
......@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: cas_server\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-24 17:18+0200\n"
"PO-Revision-Date: 2016-08-24 17:18+0200\n"
"POT-Creation-Date: 2016-09-18 11:29+0200\n"
"PO-Revision-Date: 2016-09-18 11:30+0200\n"
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
"Language-Team: django <LL@li.org>\n"
"Language: fr\n"
......@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 1.8.8\n"
"X-Generator: Poedit 1.8.9\n"
#: apps.py:25 templates/cas_server/base.html:7
#: templates/cas_server/base.html:26
......@@ -34,37 +34,37 @@ msgstr ""
"identifiant et votre mot de passe chaque fois que vous changez de site, "
"jusqu'à ce que votre session expire ou que vous vous déconnectiez."
#: forms.py:84
#: forms.py:85
msgid "Identity provider"
msgstr "fournisseur d'identité"
#: forms.py:88 forms.py:107
#: forms.py:89 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:92
#: forms.py:93
msgid "Remember the identity provider"
msgstr "Se souvenir du fournisseur d'identité"
#: forms.py:102 models.py:594
#: forms.py:104 models.py:594
msgid "username"
msgstr "nom d'utilisateur"
#: forms.py:104
#: forms.py:108
msgid "password"
msgstr "mot de passe"
#: forms.py:126
#: forms.py:131
msgid "The credentials you provided cannot be determined to be authentic."
msgstr "Les informations transmises n'ont pas permis de vous authentifier."
#: forms.py:178
#: forms.py:183
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:192
#: forms.py:197
msgid "service"
msgstr "service"
......@@ -331,7 +331,7 @@ msgstr "Connexion"
msgid "Connect to the service"
msgstr "Se connecter au service"
#: utils.py:736
#: utils.py:744
#, python-format
msgid "\"%(value)s\" is not a valid regular expression"
msgstr "\"%(value)s\" n'est pas une expression rationnelle valide"
......@@ -339,7 +339,7 @@ msgstr "\"%(value)s\" n'est pas une expression rationnelle valide"
#: views.py:185
msgid ""
"<h3>Logout successful</h3>You have successfully logged out from the Central "
"Authentication Service. For security reasons, exit your web browser."
"Authentication Service. For security reasons, close your web browser."
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 "
......@@ -349,7 +349,7 @@ msgstr ""
#, 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 "
"of the Central Authentication Service. For security reasons, close your web "
"browser."
msgstr ""
"<h3>Déconnexion réussie</h3>Vous vous êtes déconnecté(e) de %s sessions du "
......@@ -359,7 +359,7 @@ msgstr ""
#: views.py:198
msgid ""
"<h3>Logout successful</h3>You were already logged out from the Central "
"Authentication Service. For security reasons, exit your web browser."
"Authentication Service. For security reasons, close your web browser."
msgstr ""
"<h3>Déconnexion réussie</h3>Vous étiez déjà déconnecté(e) du Service Central "
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
......@@ -375,7 +375,7 @@ msgstr ""
"ticket %(ticket)s: %(error)r"
#: views.py:500
msgid "Invalid login ticket, please retry to login"
msgid "Invalid login ticket, please try to log in again"
msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter"
#: views.py:692
......
This diff is collapsed.
This diff is collapsed.
......@@ -23,4 +23,5 @@ class Command(BaseCommand):
def handle(self, *args, **options):
models.User.clean_deleted_sessions()
models.UserAttributes.clean_old_entries()
models.NewVersionWarning.send_mails()
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0001_squashed_0021_auto_20150611_2102'),
]
operations = [
migrations.AlterField(
model_name='user',
name='date',
field=models.DateTimeField(auto_now=True),
preserve_default=True,
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0002_auto_20151212_1300'),
]
operations = [
migrations.AlterField(
model_name='servicepattern',
name='pattern',
field=models.CharField(help_text="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 '\\'.", unique=True, max_length=255, verbose_name='pattern'),
preserve_default=True,
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0003_auto_20151212_1721'),
]
operations = [
migrations.AlterModelOptions(
name='servicepattern',
options={'ordering': ('pos',), 'verbose_name': 'Service pattern', 'verbose_name_plural': 'Services patterns'},
),
migrations.AlterModelOptions(
name='user',
options={'verbose_name': 'User', 'verbose_name_plural': 'Users'},
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.9.6 on 2016-06-16 10:18
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0004_auto_20151218_1032'),
]
operations = [
migrations.AlterField(
model_name='servicepattern',
name='pos',
field=models.IntegerField(default=100, help_text='service patterns are sorted using the position attribute', verbose_name='position'),
),
migrations.CreateModel(
name='FederatedIendityProvider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('suffix', models.CharField(help_text='Suffix append to backend CAS returner username: `returned_username`@`suffix`', max_length=30, unique=True, verbose_name='suffix')),
('server_url', models.CharField(max_length=255, verbose_name='server url')),
('cas_protocol_version', models.CharField(choices=[(b'1', b'CAS 1.0'), (b'2', b'CAS 2.0'), (b'3', b'CAS 3.0'), (b'CAS_2_SAML_1_0', b'SAML 1.1')], default=b'3', help_text='Version of the CAS protocol to use when sending requests the the backend CAS', max_length=30, verbose_name='CAS protocol version')),
('verbose_name', models.CharField(help_text='Name for this identity provider displayed on the login page', max_length=255, verbose_name='verbose name')),
('pos', models.IntegerField(default=100, help_text='Identity provider are sorted using the (position, verbose name, suffix) attributes', verbose_name='position')),
('display', models.BooleanField(default=True, help_text='Display the provider on the login page', verbose_name='display')),
],
options={
'verbose_name': 'identity provider',
'verbose_name_plural': 'identity providers',
},
),
migrations.CreateModel(
name='FederatedUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=124)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cas_server.FederatedIendityProvider')),
('attributs', models.TextField(blank=True, default=None, null=True)),
('ticket', models.CharField(max_length=255)),
('last_update', models.DateTimeField(auto_now=True)),
],
),
migrations.AlterUniqueTogether(
name='federateduser',
unique_together=set([('username', 'provider')]),
),
migrations.CreateModel(
name='FederateSLO',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=30)),
('session_key', models.CharField(blank=True, max_length=40, null=True)),
('ticket', models.CharField(db_index=True, max_length=255)),
],
),
migrations.AlterUniqueTogether(
name='federateslo',
unique_together=set([('username', 'session_key', 'ticket')]),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-06 17:27
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0005_auto_20160616_1018'),
]
operations = [
migrations.AlterField(
model_name='federatediendityprovider',
name='cas_protocol_version',
field=models.CharField(choices=[(b'1', b'CAS 1.0'), (b'2', b'CAS 2.0'), (b'3', b'CAS 3.0'), (b'CAS_2_SAML_1_0', b'SAML 1.1')], default=b'3', help_text='Version of the CAS protocol to use when sending requests the the backend CAS.', max_length=30, verbose_name='CAS protocol version'),
),
migrations.AlterField(
model_name='federatediendityprovider',
name='display',
field=models.BooleanField(default=True, help_text='Display the provider on the login page.', verbose_name='display'),
),
migrations.AlterField(
model_name='federatediendityprovider',
name='pos',
field=models.IntegerField(default=100, help_text='Position of the identity provider on the login page. Identity provider are sorted using the (position, verbose name, suffix) attributes.', verbose_name='position'),
),
migrations.AlterField(
model_name='federatediendityprovider',
name='suffix',
field=models.CharField(help_text='Suffix append to backend CAS returner username: ``returned_username`` @ ``suffix``.', max_length=30, unique=True, verbose_name='suffix'),
),
migrations.AlterField(
model_name='federatediendityprovider',
name='verbose_name',
field=models.CharField(help_text='Name for this identity provider displayed on the login page.', max_length=255, verbose_name='verbose name'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-23 22:52
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0006_auto_20160706_1727'),
]
operations = [
migrations.RemoveField(
model_name='federateduser',
name='attributs',
),
migrations.RemoveField(
model_name='proxygrantingticket',
name='attributs',
),
migrations.RemoveField(
model_name='proxyticket',
name='attributs',
),
migrations.RemoveField(
model_name='serviceticket',
name='attributs',
),
migrations.AddField(
model_name='federateduser',
name='_attributs',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='proxygrantingticket',
name='_attributs',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='proxyticket',
name='_attributs',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='serviceticket',
name='_attributs',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='federatediendityprovider',
name='suffix',
field=models.CharField(help_text='Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``.', max_length=30, unique=True, verbose_name='suffix'),
),
]
# -*- 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)),
],
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-14 06:19
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0008_newversionwarning'),
]
operations = [
migrations.AlterField(
model_name='filterattributvalue',
name='attribut',
field=models.CharField(help_text='Name of the attribute which must verify pattern', max_length=255, verbose_name='attribute'),
),
migrations.AlterField(
model_name='replaceattributname',
name='name',
field=models.CharField(help_text='name of an attribute to send to the service, use * for all attributes', max_length=255, verbose_name='name'),
),
migrations.AlterField(
model_name='replaceattributname',
name='replace',
field=models.CharField(blank=True, help_text='name under which the attribute will be showto the service. empty = default name of the attribut', max_length=255, verbose_name='replace'),
),
migrations.AlterField(
model_name='replaceattributvalue',
name='attribut',
field=models.CharField(help_text='Name of the attribute for which the value must be replace', max_length=255, verbose_name='attribute'),
),
migrations.AlterField(
model_name='servicepattern',
name='user_field',
field=models.CharField(blank=True, default=b'', help_text='Name of the attribute to transmit as username, empty = login', max_length=255, verbose_name='user field'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-24 21:12
from __future__ import unicode_literals
import cas_server.utils
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0009_auto_20160814_0619'),
]
operations = [
migrations.AlterField(
model_name='filterattributvalue',
name='pattern',
field=models.CharField(help_text='a regular expression', max_length=255, validators=[cas_server.utils.regexpr_validator], verbose_name='pattern'),
),
migrations.AlterField(
model_name='replaceattributname',
name='replace',
field=models.CharField(blank=True, help_text='name under which the attribute will be show to the service. empty = default name of the attribut', max_length=255, verbose_name='replace'),
),
migrations.AlterField(
model_name='replaceattributvalue',
name='pattern',
field=models.CharField(help_text='An regular expression maching whats need to be replaced', max_length=255, validators=[cas_server.utils.regexpr_validator], verbose_name='pattern'),
),
migrations.AlterField(
model_name='servicepattern',
name='pattern',
field=models.CharField(help_text="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 '\\'.", max_length=255, unique=True, validators=[cas_server.utils.regexpr_validator], verbose_name='pattern'),
),
]
......@@ -163,6 +163,8 @@ class FederatedUser(JsonAttributes):
"""
class Meta:
unique_together = ("username", "provider")
verbose_name = _("Federated user")
verbose_name_plural = _("Federated users")
#: The user username returned by the CAS backend on successful ticket validation
username = models.CharField(max_length=124)
#: A foreign key to :class:`FederatedIendityProvider`
......@@ -233,6 +235,30 @@ class FederateSLO(models.Model):
federate_slo.delete()
@python_2_unicode_compatible
class UserAttributes(JsonAttributes):
"""
Bases: :class:`JsonAttributes`
Local cache of the user attributes, used then needed
"""
class Meta:
verbose_name = _("User attributes cache")
verbose_name_plural = _("User attributes caches")
#: The username of the user for which we cache attributes
username = models.CharField(max_length=155, unique=True)
def __str__(self):
return self.username
@classmethod
def clean_old_entries(cls):
"""Remove :class:`UserAttributes` for which no more :class:`User` exists."""
for user in cls.objects.all():
if User.objects.filter(username=user.username).count() == 0:
user.delete()
@python_2_unicode_compatible
class User(models.Model):
"""
......@@ -247,9 +273,11 @@ class User(models.Model):
#: The session key of the current authenticated user
session_key = models.CharField(max_length=40, blank=True, null=True)
#: The username of the current authenticated user
username = models.CharField(max_length=30)
username = models.CharField(max_length=250)
#: Last time the authenticated user has do something (auth, fetch ticket, etc…)
date = models.DateTimeField(auto_now=True)
#: last time the user logged
last_login = models.DateTimeField(auto_now_add=True)
def delete(self, *args, **kwargs):
"""
......@@ -269,9 +297,12 @@ class User(models.Model):
Remove :class:`User` objects inactive since more that
:django:setting:`SESSION_COOKIE_AGE` and send corresponding SingleLogOut requests.
"""
users = cls.objects.filter(
date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE))
)
filter = Q(date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE)))
if settings.CAS_TGT_VALIDITY is not None:
filter |= Q(
last_login__lt=(timezone.now() - timedelta(seconds=settings.CAS_TGT_VALIDITY))
)
users = cls.objects.filter(filter)
for user in users:
user.logout()
users.delete()
......@@ -288,9 +319,22 @@ class User(models.Model):
def attributs(self):
"""
Property.
A fresh :class:`dict` for the user attributes, using ``settings.CAS_AUTH_CLASS``
A fresh :class:`dict` for the user attributes, using ``settings.CAS_AUTH_CLASS`` if
possible, and if not, try to fallback to cached attributes (actually only used for ldap
auth class with bind password check mthode).
"""
return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs()
try:
return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs()
except NotImplementedError:
try:
user = UserAttributes.objects.get(username=self.username)
attributes = user.attributs
if attributes is not None:
return attributes
else:
return {}
except UserAttributes.DoesNotExist:
return {}
def __str__(self):
return u"%s - %s" % (self.username, self.session_key)
......@@ -433,7 +477,7 @@ class ServicePattern(models.Model):
"""
Bases: :class:`django.db.models.Model`
Allowed services pattern agains services are tested to
Allowed services pattern against services are tested to
"""
class Meta:
ordering = ("pos", )
......@@ -597,7 +641,11 @@ class Username(models.Model):
#: ForeignKey to a :class:`ServicePattern`. :class:`Username` instances for a
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.usernames`
#: attribute.
service_pattern = models.ForeignKey(ServicePattern, related_name="usernames")
service_pattern = models.ForeignKey(
ServicePattern,
related_name="usernames",
on_delete=models.CASCADE
)
def __str__(self):
return self.value
......@@ -632,7 +680,11 @@ class ReplaceAttributName(models.Model):
#: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributName` instances for a
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.attributs`
#: attribute.
service_pattern = models.ForeignKey(ServicePattern, related_name="attributs")
service_pattern = models.ForeignKey(
ServicePattern,
related_name="attributs",
on_delete=models.CASCADE
)
def __str__(self):
if not self.replace:
......@@ -667,7 +719,11 @@ class FilterAttributValue(models.Model):
#: ForeignKey to a :class:`ServicePattern`. :class:`FilterAttributValue` instances for a
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.filters`
#: attribute.
service_pattern = models.ForeignKey(ServicePattern, related_name="filters")
service_pattern = models.ForeignKey(
ServicePattern,
related_name="filters",
on_delete=models.CASCADE
)
def __str__(self):
return u"%s %s" % (self.attribut, self.pattern)
......@@ -704,7 +760,11 @@ class ReplaceAttributValue(models.Model):
#: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributValue` instances for a
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.replacements`
#: attribute.
service_pattern = models.ForeignKey(ServicePattern, related_name="replacements")
service_pattern = models.ForeignKey(
ServicePattern,
related_name="replacements",
on_delete=models.CASCADE
)
def __str__(self):
return u"%s %s %s" % (self.attribut, self.pattern, self.replace)
......@@ -720,14 +780,18 @@ class Ticket(JsonAttributes):
class Meta:
abstract = True
#: ForeignKey to a :class:`User`.
user = models.ForeignKey(User, related_name="%(class)s")
user = models.ForeignKey(User, related_name="%(class)s", on_delete=models.CASCADE)
#: A boolean. ``True`` if the ticket has been validated
validate = models.BooleanField(default=False)
#: The service url for the ticket
service = models.TextField()
#: ForeignKey to a :class:`ServicePattern`. The :class:`ServicePattern` corresponding to
#: :attr:`service`. Use :meth:`ServicePattern.validate` to find it.
service_pattern = models.ForeignKey(ServicePattern, related_name="%(class)s")
service_pattern = models.ForeignKey(
ServicePattern,
related_name="%(class)s",
on_delete=models.CASCADE
)
#: Date of the ticket creation
creation = models.DateTimeField(auto_now_add=True)
#: A boolean. ``True`` if the user has just renew his authentication
......@@ -990,7 +1054,7 @@ class Proxy(models.Model):
#: ForeignKey to a :class:`ProxyTicket`. :class:`Proxy` instances for a
#: :class:`ProxyTicket` are accessible thought its :attr:`ProxyTicket.proxies`
#: attribute.
proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies")
proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies", on_delete=models.CASCADE)
def __str__(self):
return self.url
......@@ -1033,7 +1097,7 @@ 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
https://pypi.org/project/django-cas-server/
After upgrade, do not forget to run:
* ./manage.py migrate
......
{% load i18n %}{% load staticfiles %}{% get_current_language as LANGUAGE_CODE %}<!DOCTYPE html>
{% load i18n %}{% load static %}{% get_current_language as LANGUAGE_CODE %}<!DOCTYPE html>
<html{% if LANGUAGE_CODE %} lang="{{LANGUAGE_CODE}}"{% endif %}>
<head>
<meta charset="utf-8">
......@@ -58,7 +58,7 @@
class="alert alert-danger"
{% endif %}
{% endspaceless %}>
<p>{{message|safe}}</p>
<p>{{message}}</p>
</div>
{% endfor %}
{% if auto_submit %}</noscript>{% endif %}
......@@ -71,7 +71,7 @@
<div style="clear: both;"></div>
{% if settings.CAS_SHOW_POWERED %}
<div id="footer">
<p><a class="text-muted" href="https://pypi.python.org/pypi/django-cas-server">django-cas-server powered</a></p>
<p><a class="text-muted" href="https://pypi.org/project/django-cas-server/">django-cas-server powered</a></p>
</div>
{% endif %}
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
......@@ -94,6 +94,6 @@ discard_and_remember("#info-{{msg.name}}", "cas-info-{{msg.name}}", "{{msg.hash}
<!--
Powered by django-cas-server version {{VERSION}}
Pypi: https://pypi.python.org/pypi/django-cas-server
Pypi: https://pypi.org/project/django-cas-server/
github: https://github.com/nitmir/django-cas-server
-->