Add ldap bind auth method and CAS_TGT_VALIDITY parameter. Fix #18
This commit is contained in:
parent
e77dbbcd03
commit
f1fed48b21
|
@ -12,6 +12,9 @@ Unreleased
|
||||||
Added
|
Added
|
||||||
-----
|
-----
|
||||||
* Add a test for login with missing parameter (username or password or both)
|
* 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
|
Fixed
|
||||||
-----
|
-----
|
||||||
|
|
17
README.rst
17
README.rst
|
@ -268,6 +268,11 @@ Authentication settings
|
||||||
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
|
which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should
|
||||||
reduce it to something like ``86400`` seconds (1 day).
|
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
|
* ``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
|
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
|
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
|
The hashed password in the database is compare to the hexadecimal digest of the clear
|
||||||
password hashed with the corresponding algorithm.
|
password hashed with the corresponding algorithm.
|
||||||
* ``"plain"``, the password in the database must be in clear.
|
* ``"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"``.
|
The default is ``"ldap"``.
|
||||||
* ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to
|
* ``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
|
username and attributes. The user is now logged in on ``django-cas-server`` and can use
|
||||||
services using ``django-cas-server`` as CAS.
|
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.
|
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.
|
With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers.
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,12 @@
|
||||||
#
|
#
|
||||||
# (c) 2015-2016 Valentin Samir
|
# (c) 2015-2016 Valentin Samir
|
||||||
"""module for the admin interface of the app"""
|
"""module for the admin interface of the app"""
|
||||||
|
from .default_settings import settings
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern
|
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern
|
||||||
from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterAttributValue
|
from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterAttributValue
|
||||||
from .models import FederatedIendityProvider
|
from .models import FederatedIendityProvider, FederatedUser, UserAttributes
|
||||||
from .forms import TicketForm
|
from .forms import TicketForm
|
||||||
|
|
||||||
|
|
||||||
|
@ -167,6 +169,33 @@ class FederatedIendityProviderAdmin(admin.ModelAdmin):
|
||||||
list_display = ('verbose_name', 'suffix', 'display')
|
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(ServicePattern, ServicePatternAdmin)
|
||||||
admin.site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)
|
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:
|
except ImportError:
|
||||||
ldap3 = None
|
ldap3 = None
|
||||||
|
|
||||||
from .models import FederatedUser
|
from .models import FederatedUser, UserAttributes
|
||||||
from .utils import check_password, dictfetchall
|
from .utils import check_password, dictfetchall
|
||||||
|
|
||||||
|
|
||||||
|
@ -284,6 +284,10 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
|
||||||
def __init__(self, username):
|
def __init__(self, username):
|
||||||
if not ldap3:
|
if not ldap3:
|
||||||
raise RuntimeError("Please install ldap3 before using the LdapAuthUser backend")
|
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
|
# in case we got deconnected from the database, retry to connect 2 times
|
||||||
for retry_nb in range(3):
|
for retry_nb in range(3):
|
||||||
try:
|
try:
|
||||||
|
@ -294,6 +298,8 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
|
||||||
attributes=ldap3.ALL_ATTRIBUTES
|
attributes=ldap3.ALL_ATTRIBUTES
|
||||||
) and len(conn.entries) == 1:
|
) and len(conn.entries) == 1:
|
||||||
user = conn.entries[0].entry_get_attributes_dict()
|
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):
|
if user.get(settings.CAS_LDAP_USERNAME_ATTR):
|
||||||
self.user = user
|
self.user = user
|
||||||
super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0])
|
super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0])
|
||||||
|
@ -315,7 +321,34 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
|
||||||
correct, ``False`` otherwise.
|
correct, ``False`` otherwise.
|
||||||
:rtype: bool
|
: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(
|
return check_password(
|
||||||
settings.CAS_LDAP_PASSWORD_CHECK,
|
settings.CAS_LDAP_PASSWORD_CHECK,
|
||||||
password,
|
password,
|
||||||
|
@ -325,6 +358,22 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def attributs(self):
|
||||||
|
"""
|
||||||
|
The user attributes.
|
||||||
|
|
||||||
|
:return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode`
|
||||||
|
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
|
class DjangoAuthUser(AuthUser): # pragma: no cover
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -58,6 +58,10 @@ CAS_SLO_MAX_PARALLEL_REQUESTS = 10
|
||||||
CAS_SLO_TIMEOUT = 5
|
CAS_SLO_TIMEOUT = 5
|
||||||
#: Shared to transmit then using the view :class:`cas_server.views.Auth`
|
#: Shared to transmit then using the view :class:`cas_server.views.Auth`
|
||||||
CAS_AUTH_SHARED_SECRET = ''
|
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
|
#: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time
|
||||||
|
|
|
@ -23,4 +23,5 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
models.User.clean_deleted_sessions()
|
models.User.clean_deleted_sessions()
|
||||||
|
models.UserAttributes.clean_old_entries()
|
||||||
models.NewVersionWarning.send_mails()
|
models.NewVersionWarning.send_mails()
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- 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:
|
class Meta:
|
||||||
unique_together = ("username", "provider")
|
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
|
#: The user username returned by the CAS backend on successful ticket validation
|
||||||
username = models.CharField(max_length=124)
|
username = models.CharField(max_length=124)
|
||||||
#: A foreign key to :class:`FederatedIendityProvider`
|
#: A foreign key to :class:`FederatedIendityProvider`
|
||||||
|
@ -233,6 +235,30 @@ class FederateSLO(models.Model):
|
||||||
federate_slo.delete()
|
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
|
@python_2_unicode_compatible
|
||||||
class User(models.Model):
|
class User(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -250,6 +276,8 @@ class User(models.Model):
|
||||||
username = models.CharField(max_length=30)
|
username = models.CharField(max_length=30)
|
||||||
#: Last time the authenticated user has do something (auth, fetch ticket, etc…)
|
#: Last time the authenticated user has do something (auth, fetch ticket, etc…)
|
||||||
date = models.DateTimeField(auto_now=True)
|
date = models.DateTimeField(auto_now=True)
|
||||||
|
#: last time the user logged
|
||||||
|
last_login = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -269,9 +297,12 @@ class User(models.Model):
|
||||||
Remove :class:`User` objects inactive since more that
|
Remove :class:`User` objects inactive since more that
|
||||||
:django:setting:`SESSION_COOKIE_AGE` and send corresponding SingleLogOut requests.
|
:django:setting:`SESSION_COOKIE_AGE` and send corresponding SingleLogOut requests.
|
||||||
"""
|
"""
|
||||||
users = cls.objects.filter(
|
filter = Q(date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE)))
|
||||||
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:
|
for user in users:
|
||||||
user.logout()
|
user.logout()
|
||||||
users.delete()
|
users.delete()
|
||||||
|
@ -288,9 +319,22 @@ class User(models.Model):
|
||||||
def attributs(self):
|
def attributs(self):
|
||||||
"""
|
"""
|
||||||
Property.
|
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):
|
def __str__(self):
|
||||||
return u"%s - %s" % (self.username, self.session_key)
|
return u"%s - %s" % (self.username, self.session_key)
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- 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
|
||||||
|
|
||||||
|
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)
|
).update(date=new_date)
|
||||||
return client
|
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
|
@staticmethod
|
||||||
def get_user(client):
|
def get_user(client):
|
||||||
"""return the user associated with an authenticated 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")
|
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')
|
@override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser')
|
||||||
class UserTestCase(TestCase, UserModels):
|
class UserTestCase(TestCase, UserModels):
|
||||||
"""tests for the user models"""
|
"""tests for the user models"""
|
||||||
|
@ -144,6 +162,24 @@ class UserTestCase(TestCase, UserModels):
|
||||||
# assert the user has being well delete
|
# assert the user has being well delete
|
||||||
self.assertEqual(len(models.User.objects.all()), 0)
|
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).last_login < (
|
||||||
|
timezone.now() - timedelta(seconds=settings.CAS_TGT_VALIDITY)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# delete old inactive users
|
||||||
|
models.User.clean_old_entries()
|
||||||
|
# assert the user has being well delete
|
||||||
|
self.assertEqual(len(models.User.objects.all()), 0)
|
||||||
|
|
||||||
def test_clean_deleted_sessions(self):
|
def test_clean_deleted_sessions(self):
|
||||||
"""test clean_deleted_sessions"""
|
"""test clean_deleted_sessions"""
|
||||||
# get an authenticated client
|
# get an authenticated client
|
||||||
|
@ -177,6 +213,24 @@ class UserTestCase(TestCase, UserModels):
|
||||||
self.assertFalse(models.ServiceTicket.objects.all())
|
self.assertFalse(models.ServiceTicket.objects.all())
|
||||||
self.assertTrue(client2.session.get("authenticated"))
|
self.assertTrue(client2.session.get("authenticated"))
|
||||||
|
|
||||||
|
@override_settings(CAS_AUTH_CLASS='cas_server.tests.auth.TestCachedAttributesAuthUser')
|
||||||
|
def test_cached_attributs(self):
|
||||||
|
"""
|
||||||
|
Test gettting user attributes from cache for auth method that do not support direct
|
||||||
|
fetch (link the ldap bind auth methode)
|
||||||
|
"""
|
||||||
|
client = get_auth_client()
|
||||||
|
user = self.get_user(client)
|
||||||
|
# if no cache is defined, the attributes are empty
|
||||||
|
self.assertEqual(user.attributs, {})
|
||||||
|
user_attr = models.UserAttributes.objects.create(username=settings.CAS_TEST_USER)
|
||||||
|
# if a cache is defined but without atrributes, also empty
|
||||||
|
self.assertEqual(user.attributs, {})
|
||||||
|
user_attr.attributs = settings.CAS_TEST_ATTRIBUTES
|
||||||
|
user_attr.save()
|
||||||
|
# attributes are what is found in the cache
|
||||||
|
self.assertEqual(user.attributs, settings.CAS_TEST_ATTRIBUTES)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser')
|
@override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser')
|
||||||
class TicketTestCase(TestCase, UserModels, BaseServicePattern):
|
class TicketTestCase(TestCase, UserModels, BaseServicePattern):
|
||||||
|
|
|
@ -506,6 +506,7 @@ class LoginView(View, LogoutMixin):
|
||||||
username=self.request.session['username'],
|
username=self.request.session['username'],
|
||||||
session_key=self.request.session.session_key
|
session_key=self.request.session.session_key
|
||||||
)[0]
|
)[0]
|
||||||
|
self.user.last_login = timezone.now()
|
||||||
self.user.save()
|
self.user.save()
|
||||||
elif ret == self.USER_LOGIN_FAILURE: # bad user login
|
elif ret == self.USER_LOGIN_FAILURE: # bad user login
|
||||||
if settings.CAS_FEDERATE:
|
if settings.CAS_FEDERATE:
|
||||||
|
|
Loading…
Reference in New Issue