Project

General

Profile

0001-ldap-log-controls-on-authenticate-and-enable-ppolicy.patch

Loïc Dachary, 11 February 2021 11:05 AM

Download (13.5 KB)

View differences:

Subject: [PATCH] ldap: log controls on authenticate and enable ppolicy

License: MIT
 src/authentic2/backends/ldap_backend.py       |  50 ++++-
 .../locale/fr/LC_MESSAGES/django.po           |  50 +++++
 tests/test_ldap.py                            | 205 ++++++++++++++++++
 3 files changed, 302 insertions(+), 3 deletions(-)
src/authentic2/backends/ldap_backend.py
23 23
    from ldap.filter import filter_format
24 24
    from ldap.dn import escape_dn_chars
25 25
    from ldap.ldapobject import ReconnectLDAPObject as NativeLDAPObject
26
    from ldap.controls import SimplePagedResultsControl
26
    from ldap.controls import SimplePagedResultsControl, DecodeControlTuples
27
    from ldap.controls import ppolicy
28
    from pyasn1.codec.der import decoder
27 29
    PYTHON_LDAP3 = [int(x) for x in ldap.__version__.split('.')] >= [3]
28 30
    LDAPObject = NativeLDAPObject
29 31
except ImportError:
......
46 48
from django.utils.encoding import force_bytes, force_text
47 49
from django.utils import six
48 50
from django.utils.six.moves.urllib import parse as urlparse
51
from django.utils.translation import ugettext_lazy as _, ngettext
49 52

  
50 53
from authentic2.a2_rbac.models import Role
51 54

  
......
232 235
    raise NotImplementedError
233 236

  
234 237

  
238
def password_policy_control_messages(ctrl):
239
    messages = []
240

  
241
    if ctrl.error:
242
        error = ppolicy.PasswordPolicyError.namedValues[ctrl.error]
243
        error2message = {
244
            'passwordExpired': _('The password expired'),
245
            'accountLocked': _('The account is locked.'),
246
            'changeAfterReset': _('The password was reset and must be changed.'),
247
            'passwordModNotAllowed': _('It is not possible to modify the password.'),
248
            'mustSupplyOldPassword': _('The old password must be supplied.'),
249
            'insufficientPasswordQuality': _('The password does not meet the quality requirements.'),
250
            'passwordTooShort': _('The password is too short.'),
251
            'passwordTooYoung': _('It is too soon to change the password.'),
252
            'passwordInHistory': _('This password was recently used and cannot be used again.'),
253
        }
254
        messages.append(error2message[error])
255
        return messages
256

  
257
    if ctrl.timeBeforeExpiration:
258
        messages.append(_(f'The password will expire in {ctrl.timeBeforeExpiration} seconds.'))
259
    if ctrl.graceAuthNsRemaining:
260
        messages.append(ngettext(
261
            f'This is the last time this password can be used.',
262
            f'This password can only be used {ctrl.graceAuthNsRemaining} times, including this one.',
263
            ctrl.graceAuthNsRemaining))
264
    return messages
265

  
235 266
class LDAPUser(User):
236 267
    SESSION_LDAP_DATA_KEY = 'ldap-data'
237 268
    _changed = False
......
537 568
        log.debug('got config %r', blocks)
538 569
        return blocks
539 570

  
571
    @staticmethod
572
    def process_controls(authz_id, ctrls):
573
        for c in ctrls:
574
            if c.controlType == ppolicy.PasswordPolicyControl.controlType:
575
                log.info('%s: %s', authz_id, password_policy_control_messages(c))
576
            else:
577
                log.info('%s: %s', authz_id, vars(c))
578

  
540 579
    def authenticate(self, request=None, username=None, password=None, realm=None, ou=None):
541 580
        if username is None or password is None:
542 581
            return None
......
628 667
                        if failed:
629 668
                            continue
630 669
                        try:
631
                            conn.simple_bind_s(authz_id, password)
670
                            results = conn.simple_bind_s(authz_id, password, serverctrls=[
671
                                ppolicy.PasswordPolicyControl()
672
                            ])
673
                            self.process_controls(authz_id, results[3])
632 674
                            user_login_success(authz_id)
633 675
                            if not block['connect_with_user_credentials']:
634 676
                                try:
......
637 679
                                    log.exception(u'rebind failure after login bind')
638 680
                                    raise ldap.SERVER_DOWN
639 681
                            break
640
                        except ldap.INVALID_CREDENTIALS:
682
                        except ldap.INVALID_CREDENTIALS as e:
683
                            if len(e.args) > 0 and 'ctrls' in e.args[0]:
684
                                self.process_controls(authz_id, DecodeControlTuples(e.args[0]['ctrls']))
641 685
                            user_login_failure(authz_id)
642 686
                            pass
643 687
                    else:
src/authentic2/locale/fr/LC_MESSAGES/django.po
824 824
msgid "Username or email"
825 825
msgstr "Identifiant ou courriel"
826 826

  
827
#: src/authentic2/backends/ldap_backend.py:244
828
msgid "The password expired"
829
msgstr "Le mot de passe a expiré."
830

  
831
#: src/authentic2/backends/ldap_backend.py:245
832
msgid "The account is locked."
833
msgstr "Le compte est bloqué."
834

  
835
#: src/authentic2/backends/ldap_backend.py:246
836
msgid "The password was reset and must be changed."
837
msgstr "Le mot de passe a été re-initialisé et doit être changé."
838

  
839
#: src/authentic2/backends/ldap_backend.py:247
840
msgid "It is not possible to modify the password."
841
msgstr "Il n'est pas possible de modifier le mot de passe."
842

  
843
#: src/authentic2/backends/ldap_backend.py:248
844
msgid "The old password must be supplied."
845
msgstr "L'ancien mot de passe doit être fourni."
846

  
847
#: src/authentic2/backends/ldap_backend.py:249
848
msgid "The password does not meet the quality requirements."
849
msgstr "Le mot de passe n'a pas le niveau de qualité requis."
850

  
851
#: src/authentic2/backends/ldap_backend.py:250
852
msgid "The password is too short."
853
msgstr "Le mot de passe est trop court."
854

  
855
#: src/authentic2/backends/ldap_backend.py:251
856
msgid "It is too soon to change the password."
857
msgstr "Il est trop tôt pour changer ce mot de passe."
858

  
859
#: src/authentic2/backends/ldap_backend.py:252
860
msgid "This password was recently used and cannot be used again."
861
msgstr "Ce mot de passe a été utilisé récemment et ne peut pas être utilisé de nouveau."
862

  
863
#: src/authentic2/backends/ldap_backend.py:258
864
#, python-brace-format
865
msgid "The password will expire in {ctrl.timeBeforeExpiration} seconds."
866
msgstr "Le mot de passe expirera dans {ctrl.timeBeforeExpiration} secondes."
867

  
868
#: src/authentic2/backends/ldap_backend.py:261
869
#, python-brace-format
870
msgid "This is the last time this password can be used."
871
msgid_plural ""
872
"This password can only be used {ctrl.graceAuthNsRemaining} times, including "
873
"this one."
874
msgstr[0] "C'est la dernière fois qu'il est possible d'utiliser ce mot de passe."
875
msgstr[1] "Ce mot de passe ne pourra être utilisé que {ctrl.graceAuthNsRemaining} fois, celle ci incluse."
876

  
827 877
#: src/authentic2/csv_import.py:157
828 878
msgid "Cannot detect encoding"
829 879
msgstr "Impossible de détecter l’encodage"
tests/test_ldap.py
20 20

  
21 21
import pytest
22 22
import mock
23
import time
23 24

  
24 25
import ldap
25 26
from ldap.dn import escape_dn_chars
27
from ldap.controls.ppolicy import PasswordPolicyControl
26 28

  
27 29
from ldaptools.slapd import Slapd, has_slapd
28 30
from django.contrib.auth import get_user_model
......
77 79
    with create_slapd() as s:
78 80
        yield s
79 81

  
82
@pytest.fixture
83
def slapd_ppolicy():
84
    with create_slapd() as slapd:
85
        conn = slapd.get_connection_admin()
86
        assert conn.protocol_version == ldap.VERSION3
87
        conn.modify_s(
88
            'cn=module{0},cn=config',
89
            [
90
                (ldap.MOD_ADD, 'olcModuleLoad', [
91
                    force_bytes('ppolicy')
92
                ])
93
            ])
94
        slapd.add_ldif(open('/etc/ldap/schema/ppolicy.ldif').read())
95
        slapd.add_ldif('''
96
dn: olcOverlay={0}ppolicy,olcDatabase={2}mdb,cn=config
97
objectclass: olcOverlayConfig
98
objectclass: olcPPolicyConfig
99
olcoverlay: {0}ppolicy
100
olcppolicydefault: cn=default,ou=ppolicies,o=ôrga
101
olcppolicyforwardupdates: FALSE
102
olcppolicyhashcleartext: TRUE
103
olcppolicyuselockout: TRUE
104
''')
105

  
106
        slapd.add_ldif('''
107
dn: ou=ppolicies,o=ôrga
108
objectclass: organizationalUnit
109
ou: ppolicies
110
''')
111
        yield slapd
112

  
80 113

  
81 114
@pytest.fixture
82 115
def tls_slapd():
......
872 905
    assert user.pk == user2.pk
873 906

  
874 907

  
908
def test_authenticate_ppolicy_pwdMaxFailure(slapd_ppolicy, settings, db, caplog):
909
    settings.LDAP_AUTH_SETTINGS = [{
910
        'url': [slapd_ppolicy.ldap_url],
911
        'basedn': u'o=ôrga',
912
        'use_tls': False,
913
    }]
914

  
915
    pwdMaxFailure = 2
916
    slapd_ppolicy.add_ldif(f'''
917
dn: cn=default,ou=ppolicies,o=ôrga
918
cn: default
919
objectclass: top
920
objectclass: device
921
objectclass: pwdPolicy
922
objectclass: pwdPolicyChecker
923
pwdAttribute: userPassword
924
pwdMinAge: 0
925
pwdMaxAge: 0
926
pwdInHistory: 0
927
pwdCheckQuality: 0
928
pwdMinLength: 0
929
pwdExpireWarning: 0
930
pwdGraceAuthnLimit: 0
931
pwdLockout: TRUE
932
pwdLockoutDuration: 0
933
pwdMaxFailure: {pwdMaxFailure}
934
pwdMaxRecordedFailure: 0
935
pwdFailureCountInterval: 0
936
pwdMustChange: FALSE
937
pwdAllowUserChange: FALSE
938
pwdSafeModify: FALSE
939
''')
940

  
941
    for _ in range(pwdMaxFailure):
942
        assert authenticate(username=USERNAME, password='incorrect') is None
943
        assert "failed to login" in caplog.text
944
    assert 'account is locked' not in caplog.text
945
    assert authenticate(username=USERNAME, password='incorrect') is None
946
    assert 'account is locked' in caplog.text
947

  
948

  
949
def test_authenticate_ppolicy_pwdGraceAuthnLimit(slapd_ppolicy, settings, db, caplog):
950
    settings.LDAP_AUTH_SETTINGS = [{
951
        'url': [slapd_ppolicy.ldap_url],
952
        'basedn': u'o=ôrga',
953
        'use_tls': False,
954
    }]
955

  
956
    pwdMaxAge = 1
957
    pwdGraceAuthnLimit = 2
958
    slapd_ppolicy.add_ldif(f'''
959
dn: cn=default,ou=ppolicies,o=ôrga
960
cn: default
961
objectclass: top
962
objectclass: device
963
objectclass: pwdPolicy
964
objectclass: pwdPolicyChecker
965
pwdAttribute: userPassword
966
pwdMinAge: 0
967
pwdMaxAge: {pwdMaxAge}
968
pwdInHistory: 1
969
pwdCheckQuality: 0
970
pwdMinLength: 0
971
pwdExpireWarning: 0
972
pwdGraceAuthnLimit: {pwdGraceAuthnLimit}
973
pwdLockout: TRUE
974
pwdLockoutDuration: 0
975
pwdMaxFailure: 0
976
pwdMaxRecordedFailure: 0
977
pwdFailureCountInterval: 0
978
pwdMustChange: FALSE
979
pwdAllowUserChange: TRUE
980
pwdSafeModify: FALSE
981
''')
982

  
983
    user = authenticate(username=USERNAME, password=UPASS)
984
    assert user.check_password(UPASS)
985
    password = u'ogutOmyetew4'
986
    user.set_password(password)
987

  
988
    time.sleep(pwdMaxAge * 3)
989

  
990
    assert 'used 2 time' not in caplog.text
991
    assert authenticate(username=USERNAME, password=password) is not None
992
    assert 'used 2 times' in caplog.text
993

  
994
    assert 'last time' not in caplog.text
995
    assert authenticate(username=USERNAME, password=password) is not None
996
    assert 'last time' in caplog.text
997

  
998

  
999
def test_authenticate_ppolicy_pwdExpireWarning(slapd_ppolicy, settings, db, caplog):
1000
    settings.LDAP_AUTH_SETTINGS = [{
1001
        'url': [slapd_ppolicy.ldap_url],
1002
        'basedn': u'o=ôrga',
1003
        'use_tls': False,
1004
    }]
1005

  
1006
    pwdMaxAge = 3600
1007
    slapd_ppolicy.add_ldif(f'''
1008
dn: cn=default,ou=ppolicies,o=ôrga
1009
cn: default
1010
objectclass: top
1011
objectclass: device
1012
objectclass: pwdPolicy
1013
objectclass: pwdPolicyChecker
1014
pwdAttribute: userPassword
1015
pwdMinAge: 0
1016
pwdMaxAge: {pwdMaxAge}
1017
pwdInHistory: 1
1018
pwdCheckQuality: 0
1019
pwdMinLength: 0
1020
pwdExpireWarning: {pwdMaxAge}
1021
pwdGraceAuthnLimit: 0
1022
pwdLockout: TRUE
1023
pwdLockoutDuration: 0
1024
pwdMaxFailure: 0
1025
pwdMaxRecordedFailure: 0
1026
pwdFailureCountInterval: 0
1027
pwdMustChange: FALSE
1028
pwdAllowUserChange: TRUE
1029
pwdSafeModify: FALSE
1030
''')
1031

  
1032
    user = authenticate(username=USERNAME, password=UPASS)
1033
    assert user.check_password(UPASS)
1034
    password = u'ogutOmyetew4'
1035
    user.set_password(password)
1036

  
1037
    time.sleep(2)
1038

  
1039
    assert 'password will expire' not in caplog.text
1040
    assert authenticate(username=USERNAME, password=password) is not None
1041
    assert 'password will expire' in caplog.text
1042

  
1043

  
1044
def test_authenticate_ppolicy_pwdAllowUserChange(slapd_ppolicy, settings, db, caplog):
1045
    settings.LDAP_AUTH_SETTINGS = [{
1046
        'url': [slapd_ppolicy.ldap_url],
1047
        'basedn': u'o=ôrga',
1048
        'use_tls': False,
1049
    }]
1050

  
1051
    slapd_ppolicy.add_ldif(f'''
1052
dn: cn=default,ou=ppolicies,o=ôrga
1053
cn: default
1054
objectclass: top
1055
objectclass: device
1056
objectclass: pwdPolicy
1057
pwdAttribute: userPassword
1058
pwdMinAge: 0
1059
pwdMaxAge: 0
1060
pwdInHistory: 0
1061
pwdCheckQuality: 0
1062
pwdMinLength: 0
1063
pwdExpireWarning: 0
1064
pwdGraceAuthnLimit: 0
1065
pwdLockout: TRUE
1066
pwdLockoutDuration: 0
1067
pwdMaxFailure: 0
1068
pwdMaxRecordedFailure: 0
1069
pwdFailureCountInterval: 0
1070
pwdMustChange: FALSE
1071
pwdAllowUserChange: FALSE
1072
pwdSafeModify: FALSE
1073
''')
1074

  
1075
    user = authenticate(username=USERNAME, password=UPASS)
1076
    with pytest.raises(ldap.STRONG_AUTH_REQUIRED):
1077
        user.set_password(u'ogutOmyetew4')
1078

  
1079

  
875 1080
def test_ou_selector(slapd, settings, app, ou1):
876 1081
    settings.LDAP_AUTH_SETTINGS = [{
877 1082
        'url': [slapd.ldap_url],
878
-