Commit aa2a35b2 authored by Valentin Samir's avatar Valentin Samir Committed by GitHub

Merge pull request #22 from nitmir/dev

Update version to 0.8.0

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
parents ebfac168 00d47790
Pipeline #608 failed with stage
......@@ -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,28 @@ All notable changes to this project will be documented in this file.
.. contents:: Table of Contents
:depth: 2
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
......
......@@ -268,6 +268,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 +421,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 +598,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.
......@@ -638,8 +655,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 = '0.8.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)
......@@ -30,7 +30,7 @@ try: # pragma: no cover
except ImportError:
ldap3 = None
from .models import FederatedUser
from .models import FederatedUser, UserAttributes
from .utils import check_password, dictfetchall
......@@ -49,7 +49,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 +74,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 +102,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 +149,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 +188,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 +208,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 +238,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
......@@ -284,6 +284,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:
......@@ -294,6 +298,8 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
attributes=ldap3.ALL_ATTRIBUTES
) and len(conn.entries) == 1:
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])
......@@ -308,14 +314,41 @@ class LdapAuthUser(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
correct, ``False`` otherwise.
:rtype: bool
"""
if self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR):
if 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:
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.LDAPBindError, ldap3.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 +358,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 +396,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 +475,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.
......
......@@ -58,6 +58,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
......
......@@ -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.
......
......@@ -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
......
......@@ -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 -*-
# Generated by Django 1.10.1 on 2016-10-07 12:58
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0010_auto_20160824_2112'),
]
operations = [
migrations.CreateModel(
name='UserAttributes',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('_attributs', models.TextField(blank=True, default=None, null=True)),
('username', models.CharField(max_length=155, unique=True)),
],
options={
'verbose_name': 'User attributes cache',
'verbose_name_plural': 'User attributes caches',
},
),
migrations.AlterModelOptions(
name='federateduser',
options={'verbose_name': 'Federated user', 'verbose_name_plural': 'Federated users'},
),
migrations.AddField(
model_name='user',
name='last_login',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]
......@@ -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):
"""
......@@ -250,6 +276,8 @@ class User(models.Model):
username = models.CharField(max_length=30)
#: 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", )
......
......@@ -6,13 +6,13 @@
</div>
{% endfor %}
{% for field in form %}{% if not field|is_hidden %}
<div class="form-group{% spaceless %}
<div class="form-group
{% if not form.non_field_errors %}
{% if field.errors %} has-error
{% elif form.cleaned_data %} has-success
{% endif %}
{% endif %}"
{% endspaceless %}>{% spaceless %}
>{% spaceless %}
{% if field|is_checkbox %}
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label></div>
{% else %}
......
# -*- coding: utf-8 -*-
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
# more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2016 Valentin Samir
"""Some test authentication classes for the CAS"""
from cas_server import auth
class TestCachedAttributesAuthUser(auth.TestAuthUser):
"""
A test authentication class only working for one unique user.
:param unicode username: A username, stored in the :attr:`username<AuthUser.username>`
class attribute. The uniq valid value is ``settings.CAS_TEST_USER``.
"""
def attributs(self):
"""
The user attributes.
:raises NotImplementedError: as this class do not support fetching user attributes
"""
raise NotImplementedError()
......@@ -185,6 +185,17 @@ class UserModels(object):
).update(date=new_date)
return client
@staticmethod
def tgt_expired_user(sec):
"""return a user logged since sec seconds"""
client = get_auth_client()
new_date = timezone.now() - timedelta(seconds=(sec))
models.User.objects.filter(
username=settings.CAS_TEST_USER,
session_key=client.session.session_key
).update(last_login=new_date)
return client
@staticmethod
def get_user(client):
"""return the user associated with an authenticated client"""
......
......@@ -114,6 +114,24 @@ class FederateSLOTestCase(TestCase, UserModels):
models.FederateSLO.objects.get(username="test1@example.com")
@override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser')
class UserAttributesTestCase(TestCase, UserModels):
"""test for the user attributes cache model"""
def test_clean_old_entries(self):
"""test the clean_old_entries methode"""
client = get_auth_client()
user = self.get_user(client)
models.UserAttributes.objects.create(username=settings.CAS_TEST_USER)
# test that attribute cache is removed for non existant users
self.assertEqual(len(models.UserAttributes.objects.all()), 1)
models.UserAttributes.clean_old_entries()
self.assertEqual(len(models.UserAttributes.objects.all()), 1)
user.delete()
models.UserAttributes.clean_old_entries()
self.assertEqual(len(models.UserAttributes.objects.all()), 0)
@override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser')
class UserTestCase(TestCase, UserModels):
"""tests for the user models"""
......@@ -144,6 +162,24 @@ class UserTestCase(TestCase, UserModels):
# assert the user has being well delete
self.assertEqual(len(models.User.objects.all()), 0)
@override_settings(CAS_TGT_VALIDITY=3600)
def test_clean_old_entries_tgt_expired(self):
"""test clean_old_entiers with CAS_TGT_VALIDITY set"""
# get an authenticated client
client = self.tgt_expired_user(settings.CAS_TGT_VALIDITY + 60)
# assert the user exists before being cleaned
self.assertEqual(len(models.User.objects.all()), 1)
# assert the last lofin date is before the expiry date
self.assertTrue(
self.get_user(client).