From aef2d2316c589f49be5a46e560be34bf6cfc081a Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Fri, 9 Apr 2021 16:44:49 +0200 Subject: [PATCH] LDAPBackend: reactive user on login/synchronization if inactive (#52670) --- src/authentic2/backends/ldap_backend.py | 11 +++- .../0027_user_deactivation_reason.py | 18 +++++ src/authentic2/custom_user/models.py | 12 +++- tests/test_ldap.py | 65 +++++++++++++++---- 4 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 src/authentic2/custom_user/migrations/0027_user_deactivation_reason.py diff --git a/src/authentic2/backends/ldap_backend.py b/src/authentic2/backends/ldap_backend.py index 840c0e66..e9ce654f 100644 --- a/src/authentic2/backends/ldap_backend.py +++ b/src/authentic2/backends/ldap_backend.py @@ -37,7 +37,6 @@ import logging import os import random import time -import urllib.parse from django.conf import settings from django.contrib import messages @@ -338,6 +337,9 @@ def password_policy_control_messages(ctrl, attributes): return messages +LDAP_DEACTIVATION_REASON_ORPHANED = 'ldap-orphaned' + + class LDAPUser(User): SESSION_LDAP_DATA_KEY = 'ldap-data' _changed = False @@ -1477,6 +1479,9 @@ class LDAPBackend(object): if not is_user_authenticable(user): return None + if not user.is_active and user.deactivation_reason == LDAP_DEACTIVATION_REASON_ORPHANED: + user.mark_as_active() + user_login_success(user.get_username()) return user @@ -1562,11 +1567,11 @@ class LDAPBackend(object): for eid in UserExternalId.objects.filter( external_id__in=eids, user__is_active=True, source=block['realm'] ): - eid.user.mark_as_inactive() + eid.user.mark_as_inactive(reason=LDAP_DEACTIVATION_REASON_ORPHANED) # Handle users of old sources uei_qs = UserExternalId.objects.exclude(source__in=[block['realm'] for block in cls.get_config()]) for user in User.objects.filter(userexternalid__in=uei_qs): - user.mark_as_inactive() + user.mark_as_inactive(reason=LDAP_DEACTIVATION_REASON_ORPHANED) @classmethod def ad_encoding(cls, s): diff --git a/src/authentic2/custom_user/migrations/0027_user_deactivation_reason.py b/src/authentic2/custom_user/migrations/0027_user_deactivation_reason.py new file mode 100644 index 00000000..6ffdc93b --- /dev/null +++ b/src/authentic2/custom_user/migrations/0027_user_deactivation_reason.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.23 on 2021-05-18 16:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('custom_user', '0026_remove_user_deleted'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='deactivation_reason', + field=models.TextField(blank=True, null=True, verbose_name='Deactivation reason'), + ), + ] diff --git a/src/authentic2/custom_user/models.py b/src/authentic2/custom_user/models.py index caef5ded..6936f731 100644 --- a/src/authentic2/custom_user/models.py +++ b/src/authentic2/custom_user/models.py @@ -177,6 +177,7 @@ class User(AbstractBaseUser, PermissionMixin): verbose_name=_('Last account deletion alert'), null=True, blank=True ) deactivation = models.DateTimeField(verbose_name=_('Deactivation datetime'), null=True, blank=True) + deactivation_reason = models.TextField(verbose_name=_('Deactivation reason'), null=True, blank=True) objects = UserManager.from_queryset(UserQuerySet)() attributes = AttributesDescriptor() @@ -360,10 +361,17 @@ class User(AbstractBaseUser, PermissionMixin): del self._a2_attributes_cache return super(User, self).refresh_from_db(*args, **kwargs) - def mark_as_inactive(self, timestamp=None): + def mark_as_active(self): + self.is_active = True + self.deactivation = None + self.deactivation_reason = None + self.save(update_fields=['is_active', 'deactivation', 'deactivation_reason']) + + def mark_as_inactive(self, timestamp=None, reason=None): self.is_active = False self.deactivation = timestamp or timezone.now() - self.save(update_fields=['is_active', 'deactivation']) + self.deactivation_reason = reason + self.save(update_fields=['is_active', 'deactivation', 'deactivation_reason']) def set_random_password(self): self.set_password(base64.b64encode(os.urandom(32)).decode('ascii')) diff --git a/tests/test_ldap.py b/tests/test_ldap.py index a0d01133..a17240f7 100644 --- a/tests/test_ldap.py +++ b/tests/test_ldap.py @@ -253,20 +253,53 @@ def test_deactivate_orphaned_users(slapd, settings, client, db): conn.delete_s(DN) ldap_backend.LDAPBackend.deactivate_orphaned_users() + list(ldap_backend.LDAPBackend.get_users()) assert ( - ldap_backend.UserExternalId.objects.filter(user__is_active=False, source=block['realm']).count() == 1 + ldap_backend.UserExternalId.objects.filter( + user__is_active=False, + source=block['realm'], + user__deactivation__isnull=False, + user__deactivation_reason='ldap-orphaned', + ).count() + == 1 ) # rename source realm - settings.LDAP_AUTH_SETTINGS = [ - {'url': [slapd.ldap_url], 'basedn': 'o=ôrga', 'use_tls': False, 'realm': 'test'} - ] + settings.LDAP_AUTH_SETTINGS = [] + ldap_backend.LDAPBackend.deactivate_orphaned_users() + list(ldap_backend.LDAPBackend.get_users()) + + assert ( + ldap_backend.UserExternalId.objects.filter( + user__is_active=False, + source=block['realm'], + user__deactivation__isnull=False, + user__deactivation_reason='ldap-orphaned', + ).count() + == 6 + ) + # reactivate users + settings.LDAP_AUTH_SETTINGS = [block] + list(ldap_backend.LDAPBackend.get_users()) ldap_backend.LDAPBackend.deactivate_orphaned_users() assert ( - ldap_backend.UserExternalId.objects.filter(user__is_active=False, source=block['realm']).count() == 6 + ldap_backend.UserExternalId.objects.filter( + user__is_active=False, + source=block['realm'], + user__deactivation__isnull=False, + user__deactivation_reason='ldap-orphaned', + ).count() + == 1 ) + assert ( + User.objects.filter( + is_active=True, deactivation_reason__isnull=True, deactivation__isnull=True + ).count() + == 5 + ) + assert User.objects.count() == 6 @pytest.mark.django_db @@ -1191,15 +1224,18 @@ def test_do_not_use_controls(slapd_ppolicy, settings, db, caplog): def test_get_ppolicy_attributes(slapd_ppolicy, settings, db): - settings.LDAP_AUTH_SETTINGS = [{ - 'url': [slapd_ppolicy.ldap_url], - 'basedn': u'o=ôrga', - 'ppolicy_dn': u'cn=default,ou=ppolicies,o=ôrga', - 'use_tls': False, - }] + settings.LDAP_AUTH_SETTINGS = [ + { + 'url': [slapd_ppolicy.ldap_url], + 'basedn': u'o=ôrga', + 'ppolicy_dn': u'cn=default,ou=ppolicies,o=ôrga', + 'use_tls': False, + } + ] pwdMaxAge = 1 - slapd_ppolicy.add_ldif(''' + slapd_ppolicy.add_ldif( + ''' dn: cn=default,ou=ppolicies,o=ôrga cn: default objectclass: top @@ -1222,7 +1258,10 @@ pwdFailureCountInterval: 0 pwdMustChange: FALSE pwdAllowUserChange: TRUE pwdSafeModify: FALSE -'''.format(pwdMaxAge=pwdMaxAge)) +'''.format( + pwdMaxAge=pwdMaxAge + ) + ) user = authenticate(username=USERNAME, password=UPASS) assert user.check_password(UPASS) -- 2.31.1