Project

General

Profile

0001-ldap-add-method-to-get-ppolicy-operational-attribute.patch

Loïc Dachary, 18 February 2021 04:15 PM

Download (9.25 KB)

View differences:

Subject: [PATCH] ldap: add method to get ppolicy operational attributes
 (#51239)

Fixes: #51239

License: MIT
 src/authentic2/backends/ldap_backend.py | 81 ++++++++++++++++++++-----
 tests/test_ldap.py                      | 49 ++++++++++++++-
 2 files changed, 114 insertions(+), 16 deletions(-)
src/authentic2/backends/ldap_backend.py
237 237
    raise NotImplementedError
238 238

  
239 239

  
240
def password_policy_control_messages(ctrl):
240
def password_policy_control_messages(ctrl, attributes):
241 241
    messages = []
242 242

  
243 243
    if ctrl.error:
244 244
        error = ppolicy.PasswordPolicyError.namedValues[ctrl.error]
245 245
        error2message = {
246
            'passwordExpired': _('The password expired'),
247
            'accountLocked': _('The account is locked.'),
248
            'changeAfterReset': _('The password was reset and must be changed.'),
249
            'passwordModNotAllowed': _('It is not possible to modify the password.'),
250
            'mustSupplyOldPassword': _('The old password must be supplied.'),
251
            'insufficientPasswordQuality': _('The password does not meet the quality requirements.'),
252
            'passwordTooShort': _('The password is too short.'),
253
            'passwordTooYoung': _('It is too soon to change the password.'),
254
            'passwordInHistory': _('This password was recently used and cannot be used again.'),
246
            'passwordExpired': _('The password expired after {pwdmaxage}').format(**attributes),
247
            'accountLocked': _('The account is locked since {pwdaccountlockedtime[0]} after {pwdmaxfailure} failures.').format(**attributes),
248
            'changeAfterReset': _('The password was reset and must be changed.').format(**attributes),
249
            'passwordModNotAllowed': _('It is not possible to modify the password.').format(**attributes),
250
            'mustSupplyOldPassword': _('The old password must be supplied.').format(**attributes),
251
            'insufficientPasswordQuality': _('The password does not meet the quality requirements.').format(**attributes),
252
            'passwordTooShort': _('The password is too short {pwdminlength}.').format(**attributes),
253
            'passwordTooYoung': _('It is too soon to change the password {pwdminage}.').format(**attributes),
254
            'passwordInHistory': _('This password is among the last {pwdhistory} password that were used and cannot be used again.').format(**attributes),
255 255
        }
256 256
        messages.append(error2message.get(error, _('Unexpected error {error}').format(error=error)))
257 257
        return messages
......
548 548
        'user_attributes': [],
549 549
        # https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#ldap-controls
550 550
        'use_controls': True,
551
        'ppolicy_dn': '',
551 552
    }
552 553
    _REQUIRED = ('url', 'basedn')
553 554
    _TO_ITERABLE = ('url', 'groupsu', 'groupstaff', 'groupactive')
......
578 579
        log.debug('got config %r', blocks)
579 580
        return blocks
580 581

  
581
    @staticmethod
582
    def process_controls(request, authz_id, ctrls):
582
    @classmethod
583
    def process_controls(cls, request, block, conn, authz_id, ctrls):
584
        attributes = cls.get_ppolicy_attributes(block, conn, authz_id)
583 585
        for c in ctrls:
584 586
            if c.controlType == ppolicy.PasswordPolicyControl.controlType:
585
                message = ' '.join(password_policy_control_messages(c))
587
                message = ' '.join(password_policy_control_messages(c, attributes))
586 588
                if request is not None:
587 589
                    messages.add_message(request, messages.WARNING, message)
588 590
            else:
......
685 687
                            else:
686 688
                                serverctrls = []
687 689
                            results = conn.simple_bind_s(authz_id, password, serverctrls=serverctrls)
688
                            self.process_controls(request, authz_id, results[3])
690
                            self.process_controls(request, block, conn, authz_id, results[3])
689 691
                            user_login_success(authz_id)
690 692
                            if not block['connect_with_user_credentials']:
691 693
                                try:
......
696 698
                            break
697 699
                        except ldap.INVALID_CREDENTIALS as e:
698 700
                            if block.get('use_controls') and len(e.args) > 0 and 'ctrls' in e.args[0]:
699
                                self.process_controls(request, authz_id, DecodeControlTuples(e.args[0]['ctrls']))
701
                                self.process_controls(request, block, conn, authz_id, DecodeControlTuples(e.args[0]['ctrls']))
700 702
                            user_login_failure(authz_id)
701 703
                            pass
702 704
                    else:
......
1041 1043
                    attributes.add(at_mapping[key])
1042 1044
        return list(set(attribute.lower() for attribute in attributes))
1043 1045

  
1046
    @classmethod
1047
    def get_ppolicy_attributes(cls, block, conn, dn):
1048

  
1049
        def get_attributes(dn, attributes):
1050
            try:
1051
                results = conn.search_s(dn, ldap.SCOPE_BASE, u'(objectclass=*)', attributes)
1052
            except ldap.LDAPError as e:
1053
                log.error('unable to retrieve attributes of dn %r: %r', dn, e)
1054
                return {}
1055
            results = cls.normalize_ldap_results(results)
1056
            attributes_results.update(results[0][1])
1057
            return attributes_results
1058

  
1059
        user_attributes = [
1060
            'pwdaccountlockedtime',
1061
            'pwdchangedtime',
1062
            'pwdfailuretime',
1063
            'pwdgraceusetime',
1064
            'pwdhistory',
1065
            'pwdreset',
1066
        ]
1067
        ppolicy_attributes = [
1068
            'pwdminage',
1069
            'pwdmaxage',
1070
            'pwdinhistory',
1071
            'pwdcheckquality',
1072
            'pwdminlength',
1073
            'pwdexpirewarning',
1074
            'pwdgraceauthnlimit',
1075
            'pwdlockout',
1076
            'pwdlockoutduration',
1077
            'pwdmaxfailure',
1078
            'pwdmaxrecordedfailure',
1079
            'pwdfailurecountinterval',
1080
            'pwdmustchange',
1081
            'pwdallowuserchange',
1082
            'pwdsafemodify',
1083
        ]
1084
        attributes_results = { k: [] for k in user_attributes + ppolicy_attributes }
1085

  
1086
        attributes_results.update(get_attributes(dn, user_attributes))
1087
        ppolicy_dn = block.get('ppolicy_dn')
1088
        if ppolicy_dn:
1089
            attributes_results.update(**get_attributes(ppolicy_dn, ppolicy_attributes))
1090

  
1091
        print(attributes_results)
1092
        return attributes_results
1093

  
1094

  
1044 1095
    @classmethod
1045 1096
    def get_ldap_attributes(cls, block, conn, dn):
1046 1097
        '''Retrieve some attributes from LDAP, add mandatory values then apply
tests/test_ldap.py
1013 1013
    ppolicy_authenticate_exactly_pwdMaxFailure(slapd_ppolicy, caplog)
1014 1014
    assert 'account is locked' not in caplog.text
1015 1015
    assert authenticate(username=USERNAME, password='incorrect') is None
1016
    assert 'account is locked' in caplog.text
1016
    assert 'account is locked since 20' in caplog.text
1017 1017

  
1018 1018

  
1019 1019
def test_do_not_use_controls(slapd_ppolicy, settings, db, caplog):
......
1035 1035
    assert 'account is locked' not in caplog.text
1036 1036

  
1037 1037

  
1038
def test_get_ppolicy_attributes(slapd_ppolicy, settings, db):
1039
    settings.LDAP_AUTH_SETTINGS = [{
1040
        'url': [slapd_ppolicy.ldap_url],
1041
        'basedn': u'o=ôrga',
1042
        'ppolicy_dn': u'cn=default,ou=ppolicies,o=ôrga',
1043
        'use_tls': False,
1044
    }]
1045

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

  
1072
    user = authenticate(username=USERNAME, password=UPASS)
1073
    assert user.check_password(UPASS)
1074
    password = u'ogutOmyetew4'
1075
    user.set_password(password)
1076

  
1077
    time.sleep(pwdMaxAge * 3)
1078

  
1079
    conn = ldap_backend.LDAPBackend.get_connection(settings.LDAP_AUTH_SETTINGS[0])
1080
    attributes = ldap_backend.LDAPBackend.get_ppolicy_attributes(settings.LDAP_AUTH_SETTINGS[0], conn, DN)
1081
    assert 'pwdchangedtime' in attributes
1082
    assert attributes['pwdmaxage'] == [str(pwdMaxAge)]
1083

  
1084

  
1038 1085
def test_authenticate_ppolicy_pwdGraceAuthnLimit(slapd_ppolicy, settings, db, caplog):
1039 1086
    settings.LDAP_AUTH_SETTINGS = [{
1040 1087
        'url': [slapd_ppolicy.ldap_url],
1041
-