Projet

Général

Profil

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

Loïc Dachary, 11 février 2021 02:30

Télécharger (9,26 ko)

Voir les différences:

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

 src/authentic2/backends/ldap_backend.py |  39 +++++-
 tests/test_ldap.py                      | 164 ++++++++++++++++++++++++
 2 files changed, 200 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
        if ppolicy.PasswordPolicyError.namedValues[ctrl.error] == 'accountLocked':
243
            messages.append(_('The account is locked.'))
244
        return messages
245

  
246
    if ctrl.timeBeforeExpiration:
247
        messages.append(_(f'The password will expire in {ctrl.timeBeforeExpiration} seconds.'))
248
    if ctrl.graceAuthNsRemaining:
249
        messages.append(ngettext(
250
            f'This is the last time this password can be used.',
251
            f'This password can only be used {ctrl.graceAuthNsRemaining} times, including this one.',
252
            ctrl.graceAuthNsRemaining))
253
    return messages
254

  
235 255
class LDAPUser(User):
236 256
    SESSION_LDAP_DATA_KEY = 'ldap-data'
237 257
    _changed = False
......
537 557
        log.debug('got config %r', blocks)
538 558
        return blocks
539 559

  
560
    @staticmethod
561
    def process_controls(authz_id, ctrls):
562
        for c in ctrls:
563
            if c.controlType == ppolicy.PasswordPolicyControl.controlType:
564
                log.info('%s: %s', authz_id, password_policy_control_messages(c))
565
            else:
566
                log.info('%s: %s', authz_id, vars(c))
567

  
540 568
    def authenticate(self, request=None, username=None, password=None, realm=None, ou=None):
541 569
        if username is None or password is None:
542 570
            return None
......
628 656
                        if failed:
629 657
                            continue
630 658
                        try:
631
                            conn.simple_bind_s(authz_id, password)
659
                            results = conn.simple_bind_s(authz_id, password, serverctrls=[
660
                                ppolicy.PasswordPolicyControl()
661
                            ])
662
                            self.process_controls(authz_id, results[3])
632 663
                            user_login_success(authz_id)
633 664
                            if not block['connect_with_user_credentials']:
634 665
                                try:
......
637 668
                                    log.exception(u'rebind failure after login bind')
638 669
                                    raise ldap.SERVER_DOWN
639 670
                            break
640
                        except ldap.INVALID_CREDENTIALS:
671
                        except ldap.INVALID_CREDENTIALS as e:
672
                            if len(e.args) > 0 and 'ctrls' in e.args[0]:
673
                                self.process_controls(authz_id, DecodeControlTuples(e.args[0]['ctrls']))
641 674
                            user_login_failure(authz_id)
642 675
                            pass
643 676
                    else:
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
    assert 'account is locked' not in caplog.text
999
    assert authenticate(username=USERNAME, password=password) is None
1000
    assert 'account is locked' in caplog.text
1001

  
1002

  
1003
def test_authenticate_ppolicy_pwdAllowUserChange(slapd_ppolicy, settings, db, caplog):
1004
    settings.LDAP_AUTH_SETTINGS = [{
1005
        'url': [slapd_ppolicy.ldap_url],
1006
        'basedn': u'o=ôrga',
1007
        'use_tls': False,
1008
    }]
1009

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

  
1034
    user = authenticate(username=USERNAME, password=UPASS)
1035
    with pytest.raises(ldap.STRONG_AUTH_REQUIRED):
1036
        user.set_password(u'ogutOmyetew4')
1037

  
1038

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