ldap: provide user-friendlier password policy error messages (#54185)
gitea/authentic/pipeline/head Build started... Details

This commit is contained in:
Paul Marillonnet 2021-07-13 10:44:24 +02:00
parent 7408e493a2
commit 246b299fef
2 changed files with 76 additions and 11 deletions

View File

@ -32,6 +32,7 @@ except ImportError:
ldap = None
PYTHON_LDAP3 = None
import base64
import datetime
import json
import logging
import os
@ -42,6 +43,7 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.humanize.templatetags.humanize import NaturalTimeFormatter
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
from django.utils.encoding import force_bytes, force_text
@ -303,20 +305,83 @@ def password_policy_control_messages(ctrl, attributes):
if ctrl.error:
error = ppolicy.PasswordPolicyError.namedValues[ctrl.error]
def add_ppolicy_user_friendly_message_values(attributes):
def humanize_duration(seconds):
if not seconds:
return
if isinstance(seconds, str):
try:
seconds = int(seconds)
except ValueError:
return
minutes, dummy = divmod(seconds, 60)
if not minutes:
return ngettext("%(count)d second", "%(count)d seconds", seconds) % {'count': seconds}
hours, dummy = divmod(minutes, 60)
if not hours:
return ngettext("%(count)d minute", "%(count)d minutes", minutes) % {'count': minutes}
days, dummy = divmod(hours, 24)
if not days:
return ngettext("%(cound)d hour", "%(count)d hours", hours) % {'count': hours}
months, dummy = divmod(hours, 30)
if not months:
return ngettext("%(count)d day", "%(count)d days", days) % {'count': days}
return ngettext("%(count)d month", "%(count)d months", months) % {'count': months}
friendly_attributes = {}
pwdmaxage_friendly = attributes['pwdmaxage']
if pwdmaxage_friendly and isinstance(pwdmaxage_friendly, list):
pwdmaxage_friendly = pwdmaxage_friendly[0]
friendly_attributes['pwdmaxage_friendly'] = (
humanize_duration(pwdmaxage_friendly) or attributes['pwdmaxage']
)
pwdminage_friendly = attributes['pwdminage']
if pwdminage_friendly and isinstance(pwdminage_friendly, list):
pwdminage_friendly = pwdminage_friendly[0]
friendly_attributes['pwdminage_friendly'] = (
humanize_duration(pwdminage_friendly) or attributes['pwdminage']
)
pwdaccountlockedtime_friendly = attributes['pwdaccountlockedtime']
if pwdaccountlockedtime_friendly and isinstance(pwdaccountlockedtime_friendly, list):
pwdaccountlockedtime_friendly = pwdaccountlockedtime_friendly[0]
try:
pwdaccountlockedtime_friendly = int(pwdaccountlockedtime_friendly)
except ValueError:
friendly_attributes['pwdaccountlockedtime_friendly'] = attributes['pwdaccountlockedtime']
else:
friendly_attributes['pwdaccountlockedtime_friendly'] = NaturalTimeFormatter.string_for(
datetime.datetime.fromtimestamp(pwdaccountlockedtime_friendly)
)
return friendly_attributes
friendly_attributes = attributes.copy()
friendly_attributes.update(add_ppolicy_user_friendly_message_values(attributes))
error2message = {
'passwordExpired': _('The password expired after {pwdmaxage}').format(**attributes),
'passwordExpired': _('The password expired after {pwdmaxage_friendly}.').format(
**friendly_attributes
),
'accountLocked': _(
'The account is locked since {pwdaccountlockedtime[0]} after {pwdmaxfailure} failures.'
).format(**attributes),
'The account has been locked {pwdaccountlockedtime_friendly} after {pwdmaxfailure} failures.'
).format(**friendly_attributes),
'changeAfterReset': _('The password was reset and must be changed.'),
'passwordModNotAllowed': _('It is not possible to modify the password.'),
'mustSupplyOldPassword': _('The old password must be supplied.'),
'insufficientPasswordQuality': _('The password does not meet the quality requirements.'),
'passwordTooShort': _('The password is too short {pwdminlength}.').format(**attributes),
'passwordTooYoung': _('It is too soon to change the password {pwdminage}.').format(**attributes),
'passwordTooShort': _(
'The password is too short, it should be at least {pwdminlength} characters long.'
).format(**friendly_attributes),
'passwordTooYoung': _(
'It is too soon to change the password, it should be {pwdminage_friendly} old.'
).format(**friendly_attributes),
'passwordInHistory': _(
'This password is among the last {pwdhistory} password that were used and cannot be used again.'
).format(**attributes),
'This password is among the last {pwdhistory} passwords that were used and cannot be used again.'
).format(**friendly_attributes),
}
messages.append(error2message.get(error, _('Unexpected error {error}').format(error=error)))
return messages

View File

@ -1170,12 +1170,12 @@ pwdSafeModify: FALSE
response.form.set('password', 'invalid')
response = response.form.submit(name='login-password-submit')
assert 'Incorrect Username or password' in str(response.pyquery('.errornotice'))
assert 'account is locked' not in str(response.pyquery('.messages'))
assert 'account has been locked' not in str(response.pyquery('.messages'))
response = app.get('/login/')
response.form.set('username', USERNAME)
response.form.set('password', 'invalid')
response = response.form.submit(name='login-password-submit')
assert 'account is locked' in str(response.pyquery('.messages'))
assert 'account has been locked' in str(response.pyquery('.messages'))
def ppolicy_authenticate_exactly_pwdMaxFailure(slapd_ppolicy, caplog):
@ -1225,9 +1225,9 @@ def test_authenticate_ppolicy_pwdMaxFailure(slapd_ppolicy, settings, db, caplog)
]
ppolicy_authenticate_exactly_pwdMaxFailure(slapd_ppolicy, caplog)
assert 'account is locked' not in caplog.text
assert 'account has been locked' not in caplog.text
assert authenticate(username=USERNAME, password='incorrect') is None
assert 'account is locked since 20' in caplog.text
assert 'account has been locked' in caplog.text
def test_do_not_use_controls(slapd_ppolicy, settings, db, caplog):