0001-ldap-log-controls-on-authenticate-and-enable-ppolicy.patch
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 |
- |