Projet

Général

Profil

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

Loïc Dachary, 08 avril 2021 13:20

Télécharger (10,3 ko)

Voir les différences:

Subject: [PATCH] ldap: add method to get ppolicy operational attributes
 (#51239)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fixes: #51239

License: MIT

https://dev.entrouvert.org/issues/51239#note-1

Un patch corrige:

* pwdUniqueAttempts est supprimé (c'est très bien expliqué dans le fil
  de discussion, merci pour le pointeur)
* va chercher les attributs de la ppolicy pour améliorer la lisibilité
  de "votre mot de passe est trop court" et éviter la question « ok mais quelle longueur
  doit-il faire doudoudidon ? »
* l'option "ppolicy_dn" est ajoutée parce que "la définition de la
  ppolicy peut-être un peu n'importe où dans l'arbre LDAP."

Mais qui ne corrige pas:

* "votre mot de passe ne respecte pas les contraintes", « oui mes
  lesquelles ? » parce que je ne pense pas que cette information soit
  disponible (ou alors je ne sais pas ou ça se trouve)
* Globalement l'expérience utilisateur avec ppolicy est loin d'être
  géniale sur ces points parce que je n'ai pas épaules pour m'attaquer
  à ça.
 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
299 299
    raise NotImplementedError
300 300

  
301 301

  
302
def password_policy_control_messages(ctrl):
302
def password_policy_control_messages(ctrl, attributes):
303 303
    messages = []
304 304

  
305 305
    if ctrl.error:
306 306
        error = ppolicy.PasswordPolicyError.namedValues[ctrl.error]
307 307
        error2message = {
308
            'passwordExpired': _('The password expired'),
309
            'accountLocked': _('The account is locked.'),
310
            'changeAfterReset': _('The password was reset and must be changed.'),
311
            'passwordModNotAllowed': _('It is not possible to modify the password.'),
312
            'mustSupplyOldPassword': _('The old password must be supplied.'),
313
            'insufficientPasswordQuality': _('The password does not meet the quality requirements.'),
314
            'passwordTooShort': _('The password is too short.'),
315
            'passwordTooYoung': _('It is too soon to change the password.'),
316
            'passwordInHistory': _('This password was recently used and cannot be used again.'),
308
            'passwordExpired': _('The password expired after {pwdmaxage}').format(**attributes),
309
            'accountLocked': _('The account is locked since {pwdaccountlockedtime[0]} after {pwdmaxfailure} failures.').format(**attributes),
310
            'changeAfterReset': _('The password was reset and must be changed.').format(**attributes),
311
            'passwordModNotAllowed': _('It is not possible to modify the password.').format(**attributes),
312
            'mustSupplyOldPassword': _('The old password must be supplied.').format(**attributes),
313
            'insufficientPasswordQuality': _('The password does not meet the quality requirements.').format(**attributes),
314
            'passwordTooShort': _('The password is too short {pwdminlength}.').format(**attributes),
315
            'passwordTooYoung': _('It is too soon to change the password {pwdminage}.').format(**attributes),
316
            'passwordInHistory': _('This password is among the last {pwdhistory} password that were used and cannot be used again.').format(**attributes),
317 317
        }
318 318
        messages.append(error2message.get(error, _('Unexpected error {error}').format(error=error)))
319 319
        return messages
......
621 621
        'user_attributes': [],
622 622
        # https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#ldap-controls
623 623
        'use_controls': True,
624
        'ppolicy_dn': '',
624 625
    }
625 626
    _REQUIRED = ('url', 'basedn')
626 627
    _TO_ITERABLE = ('url', 'groupsu', 'groupstaff', 'groupactive')
......
659 660
        log.debug('got config %r', blocks)
660 661
        return blocks
661 662

  
662
    @staticmethod
663
    def process_controls(request, authz_id, ctrls):
663
    @classmethod
664
    def process_controls(cls, request, block, conn, authz_id, ctrls):
665
        attributes = cls.get_ppolicy_attributes(block, conn, authz_id)
664 666
        for c in ctrls:
665 667
            if c.controlType == ppolicy.PasswordPolicyControl.controlType:
666
                message = ' '.join(password_policy_control_messages(c))
668
                message = ' '.join(password_policy_control_messages(c, attributes))
667 669
                if request is not None:
668 670
                    messages.add_message(request, messages.WARNING, message)
669 671
                    if c.graceAuthNsRemaining or c.timeBeforeExpiration:
......
791 793
                            else:
792 794
                                serverctrls = []
793 795
                            results = conn.simple_bind_s(authz_id, password, serverctrls=serverctrls)
794
                            self.process_controls(request, authz_id, results[3])
796
                            self.process_controls(request, block, conn, authz_id, results[3])
795 797
                            user_login_success(authz_id)
796 798
                            if not block['connect_with_user_credentials']:
797 799
                                try:
......
803 805
                        except ldap.INVALID_CREDENTIALS as e:
804 806
                            if block.get('use_controls') and len(e.args) > 0 and 'ctrls' in e.args[0]:
805 807
                                self.process_controls(
806
                                    request, authz_id, DecodeControlTuples(e.args[0]['ctrls'])
808
                                    request, block, conn, authz_id, DecodeControlTuples(e.args[0]['ctrls'])
807 809
                                )
808 810
                            attributes = self.get_ldap_attributes(block, conn, authz_id)
809 811
                            user = self.lookup_existing_user(authz_id, block, attributes)
......
1167 1169
                    attributes.add(at_mapping[key])
1168 1170
        return list(set(attribute.lower() for attribute in attributes))
1169 1171

  
1172
    @classmethod
1173
    def get_ppolicy_attributes(cls, block, conn, dn):
1174

  
1175
        def get_attributes(dn, attributes):
1176
            try:
1177
                results = conn.search_s(dn, ldap.SCOPE_BASE, u'(objectclass=*)', attributes)
1178
            except ldap.LDAPError as e:
1179
                log.error('unable to retrieve attributes of dn %r: %r', dn, e)
1180
                return {}
1181
            results = cls.normalize_ldap_results(results)
1182
            attributes_results.update(results[0][1])
1183
            return attributes_results
1184

  
1185
        user_attributes = [
1186
            'pwdaccountlockedtime',
1187
            'pwdchangedtime',
1188
            'pwdfailuretime',
1189
            'pwdgraceusetime',
1190
            'pwdhistory',
1191
            'pwdreset',
1192
        ]
1193
        ppolicy_attributes = [
1194
            'pwdminage',
1195
            'pwdmaxage',
1196
            'pwdinhistory',
1197
            'pwdcheckquality',
1198
            'pwdminlength',
1199
            'pwdexpirewarning',
1200
            'pwdgraceauthnlimit',
1201
            'pwdlockout',
1202
            'pwdlockoutduration',
1203
            'pwdmaxfailure',
1204
            'pwdmaxrecordedfailure',
1205
            'pwdfailurecountinterval',
1206
            'pwdmustchange',
1207
            'pwdallowuserchange',
1208
            'pwdsafemodify',
1209
        ]
1210
        attributes_results = { k: [] for k in user_attributes + ppolicy_attributes }
1211

  
1212
        attributes_results.update(get_attributes(dn, user_attributes))
1213
        ppolicy_dn = block.get('ppolicy_dn')
1214
        if ppolicy_dn:
1215
            attributes_results.update(**get_attributes(ppolicy_dn, ppolicy_attributes))
1216

  
1217
        print(attributes_results)
1218
        return attributes_results
1219

  
1220

  
1170 1221
    @classmethod
1171 1222
    def get_ldap_attributes(cls, block, conn, dn):
1172 1223
        """Retrieve some attributes from LDAP, add mandatory values then apply
tests/test_ldap.py
1156 1156
    ppolicy_authenticate_exactly_pwdMaxFailure(slapd_ppolicy, caplog)
1157 1157
    assert 'account is locked' not in caplog.text
1158 1158
    assert authenticate(username=USERNAME, password='incorrect') is None
1159
    assert 'account is locked' in caplog.text
1159
    assert 'account is locked since 20' in caplog.text
1160 1160

  
1161 1161

  
1162 1162
def test_do_not_use_controls(slapd_ppolicy, settings, db, caplog):
......
1180 1180
    assert 'account is locked' not in caplog.text
1181 1181

  
1182 1182

  
1183
def test_get_ppolicy_attributes(slapd_ppolicy, settings, db):
1184
    settings.LDAP_AUTH_SETTINGS = [{
1185
        'url': [slapd_ppolicy.ldap_url],
1186
        'basedn': u'o=ôrga',
1187
        'ppolicy_dn': u'cn=default,ou=ppolicies,o=ôrga',
1188
        'use_tls': False,
1189
    }]
1190

  
1191
    pwdMaxAge = 1
1192
    slapd_ppolicy.add_ldif('''
1193
dn: cn=default,ou=ppolicies,o=ôrga
1194
cn: default
1195
objectclass: top
1196
objectclass: device
1197
objectclass: pwdPolicy
1198
objectclass: pwdPolicyChecker
1199
pwdAttribute: userPassword
1200
pwdMinAge: 0
1201
pwdMaxAge: {pwdMaxAge}
1202
pwdInHistory: 1
1203
pwdCheckQuality: 0
1204
pwdMinLength: 0
1205
pwdExpireWarning: 0
1206
pwdGraceAuthnLimit: 0
1207
pwdLockout: TRUE
1208
pwdLockoutDuration: 0
1209
pwdMaxFailure: 0
1210
pwdMaxRecordedFailure: 0
1211
pwdFailureCountInterval: 0
1212
pwdMustChange: FALSE
1213
pwdAllowUserChange: TRUE
1214
pwdSafeModify: FALSE
1215
'''.format(pwdMaxAge=pwdMaxAge))
1216

  
1217
    user = authenticate(username=USERNAME, password=UPASS)
1218
    assert user.check_password(UPASS)
1219
    password = u'ogutOmyetew4'
1220
    user.set_password(password)
1221

  
1222
    time.sleep(pwdMaxAge * 3)
1223

  
1224
    conn = ldap_backend.LDAPBackend.get_connection(settings.LDAP_AUTH_SETTINGS[0])
1225
    attributes = ldap_backend.LDAPBackend.get_ppolicy_attributes(settings.LDAP_AUTH_SETTINGS[0], conn, DN)
1226
    assert 'pwdchangedtime' in attributes
1227
    assert attributes['pwdmaxage'] == [str(pwdMaxAge)]
1228

  
1229

  
1183 1230
def test_authenticate_ppolicy_pwdGraceAuthnLimit(slapd_ppolicy, settings, db, caplog):
1184 1231
    settings.LDAP_AUTH_SETTINGS = [
1185 1232
        {
1186
-