0001-ldap-optionally-collects-messages-from-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: |
... | ... | |
41 | 43 |
from django.core.cache import cache |
42 | 44 |
from django.core.exceptions import ImproperlyConfigured |
43 | 45 |
from django.conf import settings |
46 |
from django.contrib import messages |
|
44 | 47 |
from django.contrib.auth import get_user_model |
45 | 48 |
from django.contrib.auth.models import Group |
46 | 49 |
from django.utils.encoding import force_bytes, force_text |
47 | 50 |
from django.utils import six |
48 | 51 |
from django.utils.six.moves.urllib import parse as urlparse |
52 |
from django.utils.translation import ugettext as _, ngettext |
|
49 | 53 | |
50 | 54 |
from authentic2.a2_rbac.models import Role |
51 | 55 | |
... | ... | |
232 | 236 |
raise NotImplementedError |
233 | 237 | |
234 | 238 | |
239 |
def password_policy_control_messages(ctrl): |
|
240 |
messages = [] |
|
241 | ||
242 |
if ctrl.error: |
|
243 |
error = ppolicy.PasswordPolicyError.namedValues[ctrl.error] |
|
244 |
error2message = { |
|
245 |
'passwordExpired': _('The password expired'), |
|
246 |
'accountLocked': _('The account is locked.'), |
|
247 |
'changeAfterReset': _('The password was reset and must be changed.'), |
|
248 |
'passwordModNotAllowed': _('It is not possible to modify the password.'), |
|
249 |
'mustSupplyOldPassword': _('The old password must be supplied.'), |
|
250 |
'insufficientPasswordQuality': _('The password does not meet the quality requirements.'), |
|
251 |
'passwordTooShort': _('The password is too short.'), |
|
252 |
'passwordTooYoung': _('It is too soon to change the password.'), |
|
253 |
'passwordInHistory': _('This password was recently used and cannot be used again.'), |
|
254 |
} |
|
255 |
messages.append(error2message.get(error, _('Unexpected error {error}'))) |
|
256 |
return messages |
|
257 | ||
258 |
if ctrl.timeBeforeExpiration: |
|
259 |
messages.append(_(f'The password will expire in {ctrl.timeBeforeExpiration} seconds.')) |
|
260 |
if ctrl.graceAuthNsRemaining: |
|
261 |
messages.append(ngettext( |
|
262 |
f'This is the last time this password can be used.', |
|
263 |
f'This password can only be used {ctrl.graceAuthNsRemaining} times, including this one.', |
|
264 |
ctrl.graceAuthNsRemaining)) |
|
265 |
return messages |
|
266 | ||
235 | 267 |
class LDAPUser(User): |
236 | 268 |
SESSION_LDAP_DATA_KEY = 'ldap-data' |
237 | 269 |
_changed = False |
... | ... | |
537 | 569 |
log.debug('got config %r', blocks) |
538 | 570 |
return blocks |
539 | 571 | |
572 |
@staticmethod |
|
573 |
def process_controls(request, authz_id, ctrls): |
|
574 |
for c in ctrls: |
|
575 |
if c.controlType == ppolicy.PasswordPolicyControl.controlType: |
|
576 |
message = ' '.join(password_policy_control_messages(c)) |
|
577 |
log.info('%s: %s', authz_id, message) |
|
578 |
if request is not None: |
|
579 |
messages.add_message(request, messages.WARNING, message) |
|
580 |
else: |
|
581 |
log.info('%s: %s', authz_id, vars(c)) |
|
582 | ||
540 | 583 |
def authenticate(self, request=None, username=None, password=None, realm=None, ou=None): |
541 | 584 |
if username is None or password is None: |
542 | 585 |
return None |
... | ... | |
566 | 609 |
log.error( |
567 | 610 |
"account name authentication filter doesn't contain '%s'") |
568 | 611 |
continue |
569 |
user = self.authenticate_block(block, uid, password) |
|
612 |
user = self.authenticate_block(request, block, uid, password)
|
|
570 | 613 |
if user is not None: |
571 | 614 |
return user |
572 | 615 | |
573 |
def authenticate_block(self, block, username, password): |
|
616 |
def authenticate_block(self, request, block, username, password):
|
|
574 | 617 |
for conn in self.get_connections(block): |
575 | 618 |
ldap_uri = conn.get_option(ldap.OPT_URI) |
576 | 619 |
authz_ids = [] |
... | ... | |
628 | 671 |
if failed: |
629 | 672 |
continue |
630 | 673 |
try: |
631 |
conn.simple_bind_s(authz_id, password) |
|
674 |
results = conn.simple_bind_s(authz_id, password, serverctrls=[ |
|
675 |
ppolicy.PasswordPolicyControl() |
|
676 |
]) |
|
677 |
self.process_controls(request, authz_id, results[3]) |
|
632 | 678 |
user_login_success(authz_id) |
633 | 679 |
if not block['connect_with_user_credentials']: |
634 | 680 |
try: |
... | ... | |
637 | 683 |
log.exception(u'rebind failure after login bind') |
638 | 684 |
raise ldap.SERVER_DOWN |
639 | 685 |
break |
640 |
except ldap.INVALID_CREDENTIALS: |
|
686 |
except ldap.INVALID_CREDENTIALS as e: |
|
687 |
if len(e.args) > 0 and 'ctrls' in e.args[0]: |
|
688 |
self.process_controls(request, authz_id, DecodeControlTuples(e.args[0]['ctrls'])) |
|
641 | 689 |
user_login_failure(authz_id) |
642 | 690 |
pass |
643 | 691 |
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 |
... | ... | |
77 | 78 |
with create_slapd() as s: |
78 | 79 |
yield s |
79 | 80 | |
81 |
@pytest.fixture |
|
82 |
def slapd_ppolicy(): |
|
83 |
with create_slapd() as slapd: |
|
84 |
conn = slapd.get_connection_admin() |
|
85 |
assert conn.protocol_version == ldap.VERSION3 |
|
86 |
conn.modify_s( |
|
87 |
'cn=module{0},cn=config', |
|
88 |
[ |
|
89 |
(ldap.MOD_ADD, 'olcModuleLoad', [ |
|
90 |
force_bytes('ppolicy') |
|
91 |
]) |
|
92 |
]) |
|
93 |
slapd.add_ldif(open('/etc/ldap/schema/ppolicy.ldif').read()) |
|
94 |
slapd.add_ldif(''' |
|
95 |
dn: olcOverlay={0}ppolicy,olcDatabase={2}mdb,cn=config |
|
96 |
objectclass: olcOverlayConfig |
|
97 |
objectclass: olcPPolicyConfig |
|
98 |
olcoverlay: {0}ppolicy |
|
99 |
olcppolicydefault: cn=default,ou=ppolicies,o=ôrga |
|
100 |
olcppolicyforwardupdates: FALSE |
|
101 |
olcppolicyhashcleartext: TRUE |
|
102 |
olcppolicyuselockout: TRUE |
|
103 |
''') |
|
104 | ||
105 |
slapd.add_ldif(''' |
|
106 |
dn: ou=ppolicies,o=ôrga |
|
107 |
objectclass: organizationalUnit |
|
108 |
ou: ppolicies |
|
109 |
''') |
|
110 |
yield slapd |
|
111 | ||
80 | 112 | |
81 | 113 |
@pytest.fixture |
82 | 114 |
def tls_slapd(): |
... | ... | |
872 | 904 |
assert user.pk == user2.pk |
873 | 905 | |
874 | 906 | |
907 |
def test_login_ppolicy_pwdMaxFailure(slapd_ppolicy, settings, db, app): |
|
908 |
settings.LDAP_AUTH_SETTINGS = [{ |
|
909 |
'url': [slapd_ppolicy.ldap_url], |
|
910 |
'basedn': u'o=ôrga', |
|
911 |
'use_tls': False, |
|
912 |
}] |
|
913 | ||
914 |
pwdMaxFailure = 2 |
|
915 |
slapd_ppolicy.add_ldif(f''' |
|
916 |
dn: cn=default,ou=ppolicies,o=ôrga |
|
917 |
cn: default |
|
918 |
objectclass: top |
|
919 |
objectclass: device |
|
920 |
objectclass: pwdPolicy |
|
921 |
objectclass: pwdPolicyChecker |
|
922 |
pwdAttribute: userPassword |
|
923 |
pwdMinAge: 0 |
|
924 |
pwdMaxAge: 0 |
|
925 |
pwdInHistory: 0 |
|
926 |
pwdCheckQuality: 0 |
|
927 |
pwdMinLength: 0 |
|
928 |
pwdExpireWarning: 0 |
|
929 |
pwdGraceAuthnLimit: 0 |
|
930 |
pwdLockout: TRUE |
|
931 |
pwdLockoutDuration: 0 |
|
932 |
pwdMaxFailure: {pwdMaxFailure} |
|
933 |
pwdMaxRecordedFailure: 0 |
|
934 |
pwdFailureCountInterval: 0 |
|
935 |
pwdMustChange: FALSE |
|
936 |
pwdAllowUserChange: FALSE |
|
937 |
pwdSafeModify: FALSE |
|
938 |
''') |
|
939 | ||
940 |
for _ in range(pwdMaxFailure): |
|
941 |
response = app.get('/login/') |
|
942 |
response.form.set('username', USERNAME) |
|
943 |
response.form.set('password', 'invalid') |
|
944 |
response = response.form.submit(name='login-password-submit') |
|
945 |
assert 'Incorrect Username or password' in str(response.pyquery('.errornotice')) |
|
946 |
assert 'account is locked' not in str(response.pyquery('.messages')) |
|
947 |
response = app.get('/login/') |
|
948 |
response.form.set('username', USERNAME) |
|
949 |
response.form.set('password', 'invalid') |
|
950 |
response = response.form.submit(name='login-password-submit') |
|
951 |
assert 'account is locked' in str(response.pyquery('.messages')) |
|
952 | ||
953 | ||
954 |
def test_authenticate_ppolicy_pwdMaxFailure(slapd_ppolicy, settings, db, caplog): |
|
955 |
settings.LDAP_AUTH_SETTINGS = [{ |
|
956 |
'url': [slapd_ppolicy.ldap_url], |
|
957 |
'basedn': u'o=ôrga', |
|
958 |
'use_tls': False, |
|
959 |
}] |
|
960 | ||
961 |
pwdMaxFailure = 2 |
|
962 |
slapd_ppolicy.add_ldif(f''' |
|
963 |
dn: cn=default,ou=ppolicies,o=ôrga |
|
964 |
cn: default |
|
965 |
objectclass: top |
|
966 |
objectclass: device |
|
967 |
objectclass: pwdPolicy |
|
968 |
objectclass: pwdPolicyChecker |
|
969 |
pwdAttribute: userPassword |
|
970 |
pwdMinAge: 0 |
|
971 |
pwdMaxAge: 0 |
|
972 |
pwdInHistory: 0 |
|
973 |
pwdCheckQuality: 0 |
|
974 |
pwdMinLength: 0 |
|
975 |
pwdExpireWarning: 0 |
|
976 |
pwdGraceAuthnLimit: 0 |
|
977 |
pwdLockout: TRUE |
|
978 |
pwdLockoutDuration: 0 |
|
979 |
pwdMaxFailure: {pwdMaxFailure} |
|
980 |
pwdMaxRecordedFailure: 0 |
|
981 |
pwdFailureCountInterval: 0 |
|
982 |
pwdMustChange: FALSE |
|
983 |
pwdAllowUserChange: FALSE |
|
984 |
pwdSafeModify: FALSE |
|
985 |
''') |
|
986 | ||
987 |
for _ in range(pwdMaxFailure): |
|
988 |
assert authenticate(username=USERNAME, password='incorrect') is None |
|
989 |
assert "failed to login" in caplog.text |
|
990 |
assert 'account is locked' not in caplog.text |
|
991 |
assert authenticate(username=USERNAME, password='incorrect') is None |
|
992 |
assert 'account is locked' in caplog.text |
|
993 | ||
994 | ||
995 |
def test_authenticate_ppolicy_pwdGraceAuthnLimit(slapd_ppolicy, settings, db, caplog): |
|
996 |
settings.LDAP_AUTH_SETTINGS = [{ |
|
997 |
'url': [slapd_ppolicy.ldap_url], |
|
998 |
'basedn': u'o=ôrga', |
|
999 |
'use_tls': False, |
|
1000 |
}] |
|
1001 | ||
1002 |
pwdMaxAge = 1 |
|
1003 |
pwdGraceAuthnLimit = 2 |
|
1004 |
slapd_ppolicy.add_ldif(f''' |
|
1005 |
dn: cn=default,ou=ppolicies,o=ôrga |
|
1006 |
cn: default |
|
1007 |
objectclass: top |
|
1008 |
objectclass: device |
|
1009 |
objectclass: pwdPolicy |
|
1010 |
objectclass: pwdPolicyChecker |
|
1011 |
pwdAttribute: userPassword |
|
1012 |
pwdMinAge: 0 |
|
1013 |
pwdMaxAge: {pwdMaxAge} |
|
1014 |
pwdInHistory: 1 |
|
1015 |
pwdCheckQuality: 0 |
|
1016 |
pwdMinLength: 0 |
|
1017 |
pwdExpireWarning: 0 |
|
1018 |
pwdGraceAuthnLimit: {pwdGraceAuthnLimit} |
|
1019 |
pwdLockout: TRUE |
|
1020 |
pwdLockoutDuration: 0 |
|
1021 |
pwdMaxFailure: 0 |
|
1022 |
pwdMaxRecordedFailure: 0 |
|
1023 |
pwdFailureCountInterval: 0 |
|
1024 |
pwdMustChange: FALSE |
|
1025 |
pwdAllowUserChange: TRUE |
|
1026 |
pwdSafeModify: FALSE |
|
1027 |
''') |
|
1028 | ||
1029 |
user = authenticate(username=USERNAME, password=UPASS) |
|
1030 |
assert user.check_password(UPASS) |
|
1031 |
password = u'ogutOmyetew4' |
|
1032 |
user.set_password(password) |
|
1033 | ||
1034 |
time.sleep(pwdMaxAge * 3) |
|
1035 | ||
1036 |
assert 'used 2 time' not in caplog.text |
|
1037 |
assert authenticate(username=USERNAME, password=password) is not None |
|
1038 |
assert 'used 2 times' in caplog.text |
|
1039 | ||
1040 |
assert 'last time' not in caplog.text |
|
1041 |
assert authenticate(username=USERNAME, password=password) is not None |
|
1042 |
assert 'last time' in caplog.text |
|
1043 | ||
1044 | ||
1045 |
def test_authenticate_ppolicy_pwdExpireWarning(slapd_ppolicy, settings, db, caplog): |
|
1046 |
settings.LDAP_AUTH_SETTINGS = [{ |
|
1047 |
'url': [slapd_ppolicy.ldap_url], |
|
1048 |
'basedn': u'o=ôrga', |
|
1049 |
'use_tls': False, |
|
1050 |
}] |
|
1051 | ||
1052 |
pwdMaxAge = 3600 |
|
1053 |
slapd_ppolicy.add_ldif(f''' |
|
1054 |
dn: cn=default,ou=ppolicies,o=ôrga |
|
1055 |
cn: default |
|
1056 |
objectclass: top |
|
1057 |
objectclass: device |
|
1058 |
objectclass: pwdPolicy |
|
1059 |
objectclass: pwdPolicyChecker |
|
1060 |
pwdAttribute: userPassword |
|
1061 |
pwdMinAge: 0 |
|
1062 |
pwdMaxAge: {pwdMaxAge} |
|
1063 |
pwdInHistory: 1 |
|
1064 |
pwdCheckQuality: 0 |
|
1065 |
pwdMinLength: 0 |
|
1066 |
pwdExpireWarning: {pwdMaxAge} |
|
1067 |
pwdGraceAuthnLimit: 0 |
|
1068 |
pwdLockout: TRUE |
|
1069 |
pwdLockoutDuration: 0 |
|
1070 |
pwdMaxFailure: 0 |
|
1071 |
pwdMaxRecordedFailure: 0 |
|
1072 |
pwdFailureCountInterval: 0 |
|
1073 |
pwdMustChange: FALSE |
|
1074 |
pwdAllowUserChange: TRUE |
|
1075 |
pwdSafeModify: FALSE |
|
1076 |
''') |
|
1077 | ||
1078 |
user = authenticate(username=USERNAME, password=UPASS) |
|
1079 |
assert user.check_password(UPASS) |
|
1080 |
password = u'ogutOmyetew4' |
|
1081 |
user.set_password(password) |
|
1082 | ||
1083 |
time.sleep(2) |
|
1084 | ||
1085 |
assert 'password will expire' not in caplog.text |
|
1086 |
assert authenticate(username=USERNAME, password=password) is not None |
|
1087 |
assert 'password will expire' in caplog.text |
|
1088 | ||
1089 | ||
1090 |
def test_authenticate_ppolicy_pwdAllowUserChange(slapd_ppolicy, settings, db, caplog): |
|
1091 |
settings.LDAP_AUTH_SETTINGS = [{ |
|
1092 |
'url': [slapd_ppolicy.ldap_url], |
|
1093 |
'basedn': u'o=ôrga', |
|
1094 |
'use_tls': False, |
|
1095 |
}] |
|
1096 | ||
1097 |
slapd_ppolicy.add_ldif(f''' |
|
1098 |
dn: cn=default,ou=ppolicies,o=ôrga |
|
1099 |
cn: default |
|
1100 |
objectclass: top |
|
1101 |
objectclass: device |
|
1102 |
objectclass: pwdPolicy |
|
1103 |
pwdAttribute: userPassword |
|
1104 |
pwdMinAge: 0 |
|
1105 |
pwdMaxAge: 0 |
|
1106 |
pwdInHistory: 0 |
|
1107 |
pwdCheckQuality: 0 |
|
1108 |
pwdMinLength: 0 |
|
1109 |
pwdExpireWarning: 0 |
|
1110 |
pwdGraceAuthnLimit: 0 |
|
1111 |
pwdLockout: TRUE |
|
1112 |
pwdLockoutDuration: 0 |
|
1113 |
pwdMaxFailure: 0 |
|
1114 |
pwdMaxRecordedFailure: 0 |
|
1115 |
pwdFailureCountInterval: 0 |
|
1116 |
pwdMustChange: FALSE |
|
1117 |
pwdAllowUserChange: FALSE |
|
1118 |
pwdSafeModify: FALSE |
|
1119 |
''') |
|
1120 | ||
1121 |
user = authenticate(username=USERNAME, password=UPASS) |
|
1122 |
with pytest.raises(ldap.STRONG_AUTH_REQUIRED): |
|
1123 |
user.set_password(u'ogutOmyetew4') |
|
1124 | ||
1125 | ||
875 | 1126 |
def test_ou_selector(slapd, settings, app, ou1): |
876 | 1127 |
settings.LDAP_AUTH_SETTINGS = [{ |
877 | 1128 |
'url': [slapd.ldap_url], |
878 |
- |