Merge branch 'password-strength' into 'main'

Password strength

See merge request yaal/canaille!182
This commit is contained in:
Éloi Rivard 2024-10-28 21:17:47 +00:00
commit 05cc09ab74
13 changed files with 399 additions and 95 deletions

View file

@ -3,11 +3,19 @@
Added
^^^^^
- 1 new parameter : MAX_PASSWORD_LENGTH :issue:`174`
- 1 new validator : maximum password length (default 1000) :issue:`174`
- password strength progress bar :issue:`174`
- implementation of zxcvbn-rs-py which score the password strength :issue:`174`
- New security events logs :issue:`177`
- Support for Python 3.13 :pr:`186`
Changed
^^^^^^^
- Maximum Python requirement is < 3.13 (because of the password_strength_calculator : zxcvbn-rs-py)
- MIN_PASSWORD_LENGTH become a parameter :issue:`174`
- all password tests and validator are supported by password1 field :issue:`174`
- password2 (or Password confirmation) field only support "EQUAL TO PASSWORD" test :issue:`174`
- Update to HTMX 2.0.3 :pr:`184`
Removed

View file

@ -6,6 +6,8 @@ from flask import request
from flask import session
from flask_wtf.csrf import CSRFProtect
from canaille.app.forms import password_strength_calculator
csrf = CSRFProtect()
@ -28,6 +30,7 @@ def setup_sentry(app): # pragma: no cover
def setup_jinja(app):
app.jinja_env.filters["len"] = len
app.jinja_env.filters["password_strength"] = password_strength_calculator
app.jinja_env.policies["ext.i18n.trimmed"] = True

View file

@ -46,6 +46,44 @@ def phone_number(form, field):
raise wtforms.ValidationError(_("Not a valid phone number"))
def password_length_validator(form, field):
minimum_password_length = current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"]
if minimum_password_length:
if len(field.data) < minimum_password_length:
raise wtforms.ValidationError(
_(
"Field must be at least {minimum_password_length} characters long."
).format(minimum_password_length=str(minimum_password_length))
)
def password_too_long_validator(form, field):
maximum_password_length = min(
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] or 4096, 4096
)
if len(field.data) > maximum_password_length:
raise wtforms.ValidationError(
_(
"Field cannot be longer than {maximum_password_length} characters."
).format(maximum_password_length=str(maximum_password_length))
)
def password_strength_calculator(password):
try:
from zxcvbn_rs_py import zxcvbn
except ImportError:
return None
strength_score = 0
if password and type(password) is str:
strength_score = zxcvbn(password).score
strength_score = strength_score * 100 // 4
return strength_score
def email_validator(form, field):
try:
import email_validator # noqa: F401

View file

@ -298,3 +298,21 @@ class CoreSettings(BaseModel):
[CANAILLE.ACL.ADMIN]
WRITE = ["user_name", "groups"]
"""
MIN_PASSWORD_LENGTH: int = 8
"""Minimum length for user password.
Defaults to 8.
It is possible not to set a minimum, by entering None or 0.
"""
MAX_PASSWORD_LENGTH: int = 1000
"""Maximum length for user password.
Defaults to 1000.
There is a technical limit with passlib used by sql database of 4096
characters. If the value entered is 0 or None, or greater than 4096,
then 4096 will be retained.
"""

View file

@ -35,6 +35,8 @@ from canaille.app.flask import user_needed
from canaille.app.forms import IDToModel
from canaille.app.forms import TableForm
from canaille.app.forms import is_readonly
from canaille.app.forms import password_length_validator
from canaille.app.forms import password_too_long_validator
from canaille.app.forms import set_readonly
from canaille.app.forms import set_writable
from canaille.app.i18n import gettext as _
@ -47,7 +49,6 @@ from ..mails import send_invitation_mail
from ..mails import send_password_initialization_mail
from ..mails import send_password_reset_mail
from ..mails import send_registration_mail
from .forms import MINIMUM_PASSWORD_LENGTH
from .forms import EmailConfirmationForm
from .forms import InvitationForm
from .forms import JoinForm
@ -313,11 +314,11 @@ def registration(data=None, hash=None):
form["password1"].validators = [
wtforms.validators.DataRequired(),
wtforms.validators.Length(min=MINIMUM_PASSWORD_LENGTH),
password_length_validator,
password_too_long_validator,
]
form["password2"].validators = [
wtforms.validators.DataRequired(),
wtforms.validators.Length(min=MINIMUM_PASSWORD_LENGTH),
]
form["password1"].flags.required = True
form["password2"].flags.required = True

View file

@ -12,6 +12,8 @@ from canaille.app.forms import Form
from canaille.app.forms import IDToModel
from canaille.app.forms import email_validator
from canaille.app.forms import is_uri
from canaille.app.forms import password_length_validator
from canaille.app.forms import password_too_long_validator
from canaille.app.forms import phone_number
from canaille.app.forms import set_readonly
from canaille.app.forms import unique_values
@ -20,8 +22,6 @@ from canaille.app.i18n import lazy_gettext as _
from canaille.app.i18n import native_language_name_from_code
from canaille.backends import Backend
MINIMUM_PASSWORD_LENGTH = 8
def unique_user_name(form, field):
if Backend.instance.get(models.User, user_name=field.data) and (
@ -263,7 +263,8 @@ PROFILE_FORM_FIELDS = dict(
_("Password"),
validators=[
wtforms.validators.Optional(),
wtforms.validators.Length(min=MINIMUM_PASSWORD_LENGTH),
password_length_validator,
password_too_long_validator,
],
render_kw={
"autocomplete": "new-password",

View file

@ -74,6 +74,9 @@ footer a {
color: rgba(0,0,0,1);
}
.progress_bar:first-child {
margin: 1em 0 .28571429rem 0;
}
/* Fix button appearance for semantic-ui on webkit */
[type=button] {
@ -255,4 +258,7 @@ select.ui.multiple.dropdown option[selected] {
box-shadow: 0 0 0 100px #888888 inset !important;
border: 1px solid rgba(255,255,255,0.87) !important;
}
.ui.progress {
background: #222222;
}
}

View file

@ -19,7 +19,7 @@ del_button=false
{% set inline_validation = field.validators and field.type not in ("FileField", "MultipleFileField") %}
{% if inline_validation %}
{% set ignore_me = kwargs.update({"hx-post": ""}) %}
{% set ignore_me = kwargs.update({"hx-indicator": "closest .input"}) %}
{% set ignore_me = kwargs.update({"hx-indicator": "closest .input", "hx-trigger": "input changed delay:500ms"}) %}
{% endif %}
{% if container and field_visible %}
@ -114,6 +114,16 @@ del_button=false
{% endfor %}
{% endif %}
{% if field.name == "password1" and field.data|password_strength and not field.errors %}
<div>
<p class="progress_bar">{% trans %}Password strength{% endtrans %}</p>
<div class="ui indicating progress" data-percent="{{ field.data|password_strength }}">
<div class="bar" style="width: {{ field.data|password_strength }}%;">
</div>
</div>
</div>
{% endif %}
{% if container and field_visible %}
</div>
{% endif %}

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-09-12 19:28+0200\n"
"POT-Creation-Date: 2024-10-28 11:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -21,23 +21,31 @@ msgstr ""
msgid "No SMTP server has been configured"
msgstr ""
#: canaille/app/forms.py:26
#: canaille/app/forms.py:27
msgid "This is not a valid URL"
msgstr ""
#: canaille/app/forms.py:33 canaille/app/forms.py:34
#: canaille/app/forms.py:34 canaille/app/forms.py:35
msgid "This value is a duplicate"
msgstr ""
#: canaille/app/forms.py:46
#: canaille/app/forms.py:47
msgid "Not a valid phone number"
msgstr ""
#: canaille/app/forms.py:206
#: canaille/app/forms.py:55
msgid "Field must be at least {minimum_password_length} characters long."
msgstr ""
#: canaille/app/forms.py:67
msgid "Field cannot be longer than {maximum_password_length} characters."
msgstr ""
#: canaille/app/forms.py:240
msgid "The page number is not valid"
msgstr ""
#: canaille/app/forms.py:234
#: canaille/app/forms.py:268
msgid "Not a valid datetime value."
msgstr ""
@ -54,7 +62,7 @@ msgid "John Doe"
msgstr ""
#: canaille/backends/ldap/backend.py:178 canaille/core/endpoints/forms.py:164
#: canaille/core/endpoints/forms.py:423
#: canaille/core/endpoints/forms.py:424
msgid "jdoe"
msgstr ""
@ -128,8 +136,8 @@ msgstr ""
msgid "You are already logged in, you cannot create an account."
msgstr ""
#: canaille/core/endpoints/account.py:299 canaille/core/endpoints/forms.py:313
#: canaille/core/endpoints/forms.py:441 canaille/core/templates/groups.html:5
#: canaille/core/endpoints/account.py:299 canaille/core/endpoints/forms.py:314
#: canaille/core/endpoints/forms.py:442 canaille/core/templates/groups.html:5
#: canaille/core/templates/groups.html:23
#: canaille/core/templates/partial/group-members.html:15
#: canaille/core/templates/partial/users.html:18
@ -175,76 +183,76 @@ msgstr ""
msgid "User account creation succeed."
msgstr ""
#: canaille/core/endpoints/account.py:610
#: canaille/core/endpoints/account.py:771
#: canaille/core/endpoints/account.py:615
#: canaille/core/endpoints/account.py:784
msgid "Profile edition failed."
msgstr ""
#: canaille/core/endpoints/account.py:614
#: canaille/core/endpoints/account.py:786
#: canaille/core/endpoints/account.py:625
#: canaille/core/endpoints/account.py:802
msgid "Profile updated successfully."
msgstr ""
#: canaille/core/endpoints/account.py:621
#: canaille/core/endpoints/account.py:633
msgid "Email addition failed."
msgstr ""
#: canaille/core/endpoints/account.py:626
#: canaille/core/endpoints/account.py:638
msgid ""
"An email has been sent to the email address. Please check your inbox and "
"click on the verification link it contains"
msgstr ""
#: canaille/core/endpoints/account.py:633
#: canaille/core/endpoints/account.py:645
msgid "Could not send the verification email"
msgstr ""
#: canaille/core/endpoints/account.py:643
#: canaille/core/endpoints/account.py:655
msgid "Email deletion failed."
msgstr ""
#: canaille/core/endpoints/account.py:646
#: canaille/core/endpoints/account.py:658
msgid "The email have been successfully deleted."
msgstr ""
#: canaille/core/endpoints/account.py:683
#: canaille/core/endpoints/account.py:695
msgid ""
"A password initialization link has been sent at the user email address. "
"It should be received within a few minutes."
msgstr ""
#: canaille/core/endpoints/account.py:690 canaille/core/endpoints/auth.py:159
#: canaille/core/endpoints/account.py:702 canaille/core/endpoints/auth.py:159
msgid "Could not send the password initialization email"
msgstr ""
#: canaille/core/endpoints/account.py:701
#: canaille/core/endpoints/account.py:713
msgid ""
"A password reset link has been sent at the user email address. It should "
"be received within a few minutes."
msgstr ""
#: canaille/core/endpoints/account.py:708
#: canaille/core/endpoints/account.py:720
msgid "Could not send the password reset email"
msgstr ""
#: canaille/core/endpoints/account.py:724
#: canaille/core/endpoints/account.py:736
msgid "The account has been locked"
msgstr ""
#: canaille/core/endpoints/account.py:735
#: canaille/core/endpoints/account.py:747
msgid "The account has been unlocked"
msgstr ""
#: canaille/core/endpoints/account.py:806
#: canaille/core/endpoints/account.py:822
#, python-format
msgid "The user %(user)s has been successfully deleted"
msgstr ""
#: canaille/core/endpoints/account.py:823
#: canaille/core/endpoints/account.py:839
msgid "Locked users cannot be impersonated."
msgstr ""
#: canaille/core/endpoints/account.py:827 canaille/core/endpoints/auth.py:112
#: canaille/core/endpoints/account.py:843 canaille/core/endpoints/auth.py:112
#, python-format
msgid "Connection successful. Welcome %(user)s"
msgstr ""
@ -256,8 +264,8 @@ msgstr ""
#: canaille/core/endpoints/admin.py:29 canaille/core/endpoints/forms.py:97
#: canaille/core/endpoints/forms.py:120 canaille/core/endpoints/forms.py:209
#: canaille/core/endpoints/forms.py:409 canaille/core/endpoints/forms.py:435
#: canaille/core/endpoints/forms.py:459 canaille/core/endpoints/forms.py:475
#: canaille/core/endpoints/forms.py:410 canaille/core/endpoints/forms.py:436
#: canaille/core/endpoints/forms.py:460 canaille/core/endpoints/forms.py:476
msgid "jane@doe.com"
msgstr ""
@ -314,15 +322,15 @@ msgid ""
"We cannot send a password reset email."
msgstr ""
#: canaille/core/endpoints/auth.py:207
#: canaille/core/endpoints/auth.py:213
msgid "We encountered an issue while we sent the password recovery email."
msgstr ""
#: canaille/core/endpoints/auth.py:230
#: canaille/core/endpoints/auth.py:236
msgid "The password reset link that brought you here was invalid."
msgstr ""
#: canaille/core/endpoints/auth.py:239
#: canaille/core/endpoints/auth.py:245
msgid "Your password has been updated successfully"
msgstr ""
@ -368,11 +376,11 @@ msgstr ""
msgid "Password"
msgstr ""
#: canaille/core/endpoints/forms.py:136 canaille/core/endpoints/forms.py:273
#: canaille/core/endpoints/forms.py:136 canaille/core/endpoints/forms.py:274
msgid "Password confirmation"
msgstr ""
#: canaille/core/endpoints/forms.py:139 canaille/core/endpoints/forms.py:276
#: canaille/core/endpoints/forms.py:139 canaille/core/endpoints/forms.py:277
msgid "Password and confirmation do not match."
msgstr ""
@ -384,8 +392,8 @@ msgstr ""
msgid "Username"
msgstr ""
#: canaille/core/endpoints/forms.py:167 canaille/core/endpoints/forms.py:365
#: canaille/core/endpoints/forms.py:379
#: canaille/core/endpoints/forms.py:167 canaille/core/endpoints/forms.py:366
#: canaille/core/endpoints/forms.py:380
#: canaille/core/templates/partial/group-members.html:12
#: canaille/core/templates/partial/groups.html:6
#: canaille/core/templates/partial/users.html:12
@ -426,12 +434,12 @@ msgstr ""
msgid "Johnny"
msgstr ""
#: canaille/core/endpoints/forms.py:199 canaille/core/endpoints/forms.py:465
#: canaille/core/endpoints/forms.py:199 canaille/core/endpoints/forms.py:466
#: canaille/core/templates/profile_edit.html:176
msgid "Email addresses"
msgstr ""
#: canaille/core/endpoints/forms.py:205 canaille/core/endpoints/forms.py:455
#: canaille/core/endpoints/forms.py:205 canaille/core/endpoints/forms.py:456
msgid ""
"This email will be used as a recovery address to reset the password if "
"needed"
@ -491,68 +499,68 @@ msgstr ""
msgid "Delete the photo"
msgstr ""
#: canaille/core/endpoints/forms.py:284
#: canaille/core/endpoints/forms.py:285
msgid "User number"
msgstr ""
#: canaille/core/endpoints/forms.py:286 canaille/core/endpoints/forms.py:292
#: canaille/core/endpoints/forms.py:287 canaille/core/endpoints/forms.py:293
msgid "1234"
msgstr ""
#: canaille/core/endpoints/forms.py:290
#: canaille/core/endpoints/forms.py:291
msgid "Department"
msgstr ""
#: canaille/core/endpoints/forms.py:296
#: canaille/core/endpoints/forms.py:297
msgid "Organization"
msgstr ""
#: canaille/core/endpoints/forms.py:298
#: canaille/core/endpoints/forms.py:299
msgid "Cogip LTD."
msgstr ""
#: canaille/core/endpoints/forms.py:302
#: canaille/core/endpoints/forms.py:303
msgid "Website"
msgstr ""
#: canaille/core/endpoints/forms.py:304
#: canaille/core/endpoints/forms.py:305
msgid "https://mywebsite.tld"
msgstr ""
#: canaille/core/endpoints/forms.py:309
#: canaille/core/endpoints/forms.py:310
msgid "Preferred language"
msgstr ""
#: canaille/core/endpoints/forms.py:319
#: canaille/core/endpoints/forms.py:320
msgid "users, admins …"
msgstr ""
#: canaille/core/endpoints/forms.py:344
#: canaille/core/endpoints/forms.py:345
msgid "Account expiration"
msgstr ""
#: canaille/core/endpoints/forms.py:368
#: canaille/core/endpoints/forms.py:369
msgid "group"
msgstr ""
#: canaille/core/endpoints/forms.py:372 canaille/core/endpoints/forms.py:389
#: canaille/core/endpoints/forms.py:373 canaille/core/endpoints/forms.py:390
#: canaille/core/templates/partial/groups.html:7
msgid "Description"
msgstr ""
#: canaille/core/endpoints/forms.py:403 canaille/core/endpoints/forms.py:428
#: canaille/core/endpoints/forms.py:404 canaille/core/endpoints/forms.py:429
msgid "Email address"
msgstr ""
#: canaille/core/endpoints/forms.py:422
#: canaille/core/endpoints/forms.py:423
msgid "User name"
msgstr ""
#: canaille/core/endpoints/forms.py:426
#: canaille/core/endpoints/forms.py:427
msgid "Username editable by the invitee"
msgstr ""
#: canaille/core/endpoints/forms.py:468
#: canaille/core/endpoints/forms.py:469
msgid "New email address"
msgstr ""
@ -1424,29 +1432,29 @@ msgstr ""
msgid "The client has been deleted."
msgstr ""
#: canaille/oidc/endpoints/consents.py:73
#: canaille/oidc/endpoints/consents.py:108
#: canaille/oidc/endpoints/consents.py:75
#: canaille/oidc/endpoints/consents.py:114
msgid "Could not revoke this access"
msgstr ""
#: canaille/oidc/endpoints/consents.py:76
#: canaille/oidc/endpoints/consents.py:78
msgid "The access is already revoked"
msgstr ""
#: canaille/oidc/endpoints/consents.py:80
#: canaille/oidc/endpoints/consents.py:123
#: canaille/oidc/endpoints/consents.py:86
#: canaille/oidc/endpoints/consents.py:129
msgid "The access has been revoked"
msgstr ""
#: canaille/oidc/endpoints/consents.py:89
#: canaille/oidc/endpoints/consents.py:95
msgid "Could not restore this access"
msgstr ""
#: canaille/oidc/endpoints/consents.py:92
#: canaille/oidc/endpoints/consents.py:98
msgid "The access is not revoked"
msgstr ""
#: canaille/oidc/endpoints/consents.py:99
#: canaille/oidc/endpoints/consents.py:105
msgid "The access has been restored"
msgstr ""
@ -1519,15 +1527,15 @@ msgstr ""
msgid "Pre-consent"
msgstr ""
#: canaille/oidc/endpoints/oauth.py:355
#: canaille/oidc/endpoints/oauth.py:369
msgid "You have been disconnected"
msgstr ""
#: canaille/oidc/endpoints/oauth.py:372
#: canaille/oidc/endpoints/oauth.py:386
msgid "You have not been disconnected"
msgstr ""
#: canaille/oidc/endpoints/tokens.py:45
#: canaille/oidc/endpoints/tokens.py:50
msgid "The token has successfully been revoked."
msgstr ""
@ -1832,6 +1840,10 @@ msgstr ""
msgid "Add another field"
msgstr ""
#: canaille/templates/macro/form.html:119
msgid "Password strength"
msgstr ""
#: canaille/templates/macro/table.html:8
msgid "Search…"
msgstr ""

115
poetry.lock generated
View file

@ -916,17 +916,6 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "legacy-cgi"
version = "2.6.1"
description = "Fork of the standard library cgi and cgitb modules, being deprecated in PEP-594"
optional = false
python-versions = "<4.0,>=3.10"
files = [
{file = "legacy_cgi-2.6.1-py3-none-any.whl", hash = "sha256:8eacc1522d9f76451337a4b5a0abf494158d39250754b0d1bc19a14c6512af9b"},
{file = "legacy_cgi-2.6.1.tar.gz", hash = "sha256:f2ada99c747c3d72a473a6aaff6259a61f226b06fe9f3106e495ab83fd8f7a42"},
]
[[package]]
name = "lxml"
version = "5.3.0"
@ -1344,10 +1333,7 @@ files = [
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.23.4"
typing-extensions = [
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
]
typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
[package.extras]
email = ["email-validator (>=2.0.0)"]
@ -2469,9 +2455,6 @@ files = [
{file = "webob-1.8.9.tar.gz", hash = "sha256:ad6078e2edb6766d1334ec3dee072ac6a7f95b1e32ce10def8ff7f0f02d56589"},
]
[package.dependencies]
legacy-cgi = {version = ">=2.6", markers = "python_version >= \"3.13\""}
[package.extras]
docs = ["Sphinx (>=1.7.5)", "pylons-sphinx-themes"]
testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"]
@ -2530,9 +2513,97 @@ markupsafe = "*"
[package.extras]
email = ["email-validator"]
[[package]]
name = "zxcvbn-rs-py"
version = "0.1.1"
description = "Python bindings for zxcvbn-rs, the Rust implementation of zxcvbn"
optional = true
python-versions = ">=3.7, <3.13"
files = [
{file = "zxcvbn_rs_py-0.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c16d2ffa0933ac014b8e79bccfd2e9cf2ef767db047df99a7cc4597bfdf4eb2"},
{file = "zxcvbn_rs_py-0.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c010de71c57b08b8cdf04c36b5b523ca532f1533f8191b1f565d22f884490ad3"},
{file = "zxcvbn_rs_py-0.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c2fe75ffb4b6b2226c9417943fc9c175f0202cf5a9111de523417c8a0a19b60"},
{file = "zxcvbn_rs_py-0.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5ee2612f3ed959543aeed74c9886b35b1a968a0a499cca275a863ca89b700be"},
{file = "zxcvbn_rs_py-0.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eef690fd029b29bad9bb2ab2025b944aafb40ebf390f64734888808742398242"},
{file = "zxcvbn_rs_py-0.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00a405d33d3c250db6b2326c4f1050ba7cc386b1b07b5b8703ed7ccfa6965d38"},
{file = "zxcvbn_rs_py-0.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6705c5a819cfb0c95df3f7e02719830c88c354e8d7e634c83df1271fdc9c973e"},
{file = "zxcvbn_rs_py-0.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0a94936e4cf5fb56f844923e381ba35b33cc07533f48f0a97ee5e6dfb2846ee"},
{file = "zxcvbn_rs_py-0.1.1-cp310-none-win32.whl", hash = "sha256:80189ae562eeff0e1d44dd97f8de5861fccfe4799bb4751c27bd98bf5d62bf42"},
{file = "zxcvbn_rs_py-0.1.1-cp310-none-win_amd64.whl", hash = "sha256:585bffa0887fb37e80e58d1d87a6523e5b5b4c6d518941e95e4b4012cc7131da"},
{file = "zxcvbn_rs_py-0.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4364513fd68ae347382feebd68f49b4a60f81ae4e905af6a9f337d684cadce1c"},
{file = "zxcvbn_rs_py-0.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9333bfe083f6a2271174cb865ffa9541ce22c54e245a8478c46efcdfde6dea78"},
{file = "zxcvbn_rs_py-0.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff2927550ec00f9a3d4e082510ecf8124b52faf899c1eb812626ca8dc4caa4a"},
{file = "zxcvbn_rs_py-0.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02a1e488aa0f21f49f14dcdd9bb802d337dbc47c0ddf329b30333997e32a391e"},
{file = "zxcvbn_rs_py-0.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb0fb76d0a331d0655cec6fd53a4acd87a0a60f26751999267c7319d0edc8e05"},
{file = "zxcvbn_rs_py-0.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a0c3427e82b8fec06cd2b3a6932ad747bcaf1e83d6794fdd1e4a59de808283c"},
{file = "zxcvbn_rs_py-0.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c139aaa1801dfacf308024e06901e0ce9a4438b1a189c70cba9126a729523e1"},
{file = "zxcvbn_rs_py-0.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:da866bb00c855afa0ab4ac882fb7ae8fa7bbeccaafd1ab83776b290959f5ac6d"},
{file = "zxcvbn_rs_py-0.1.1-cp311-none-win32.whl", hash = "sha256:01b13c536e7058a52d0288b7d83a53108b4fbacb01bb29039570e8cd09c8cae3"},
{file = "zxcvbn_rs_py-0.1.1-cp311-none-win_amd64.whl", hash = "sha256:d5dbd8376b699c290d2a2ed4330ac1602e6ff7ff07b8d473ad81f31d0bacf279"},
{file = "zxcvbn_rs_py-0.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:502ea698acd13c07f2c52e2b092c9a44749baf26aa30c8f5d2d8d3d355d230fc"},
{file = "zxcvbn_rs_py-0.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4ddbcd63f795795f41811e126c01fb9fe8ebf3eeb436aa7bbdf6130374486bb"},
{file = "zxcvbn_rs_py-0.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28938ef0b0ed4a72c541737eb82f03e17823996a4b3d7efc88d071287378dea"},
{file = "zxcvbn_rs_py-0.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc1f1318b2d69c97d1486b8cb25e453c28d743cff49e6f03734d1d2b84378b3c"},
{file = "zxcvbn_rs_py-0.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c435b625981dcc4f47ba7835e82a8fa6d517dc023ebe67de708375e49e19f30"},
{file = "zxcvbn_rs_py-0.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33a501782fb75e296441c8901779681ba363cccb3ebe421ac416e161b51f0c13"},
{file = "zxcvbn_rs_py-0.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74119c768708993af9eddc5840da0b0ce1d97b900341c5a911663b4ae9896656"},
{file = "zxcvbn_rs_py-0.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:30a62fc1d3f284f087b35254f34ff53591b0de658ea15625ee52484a043d2e58"},
{file = "zxcvbn_rs_py-0.1.1-cp312-none-win32.whl", hash = "sha256:93ab1d484b9357d30a273a7c3ce92999686b744140af3ec790a5dc422f77542e"},
{file = "zxcvbn_rs_py-0.1.1-cp312-none-win_amd64.whl", hash = "sha256:5e658a75a1e224acea935e4f9c7b3d7734d5fecd4e41570fec20e24aea69a65d"},
{file = "zxcvbn_rs_py-0.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66a2dd6831b4fac382d71c97e8513698b922bda0d926b62d26f13f1aa56792b4"},
{file = "zxcvbn_rs_py-0.1.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c113702b0856d160b77a2c13bc70ed90fdb480355bba1ecb4f6ed2534a43656c"},
{file = "zxcvbn_rs_py-0.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4b1e70de8c2684e83ba2d3bb42162a1d934bce4a68878836ec0f4f78bb467ad"},
{file = "zxcvbn_rs_py-0.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c72c98ce471965f45755c95864e6839fbc8d7da0d23636b1499f82ea80107907"},
{file = "zxcvbn_rs_py-0.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a947cd4f49da1f6d63f106362db665733338363c0fa7e094f619ce854ff5919"},
{file = "zxcvbn_rs_py-0.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:79dd2bee5fd956d4d5cf31fca4b7fea4a59f6f3a70f252f1170d2fa6d6f0a2ae"},
{file = "zxcvbn_rs_py-0.1.1-cp37-none-win32.whl", hash = "sha256:605b9e6c4e09be5ba776dcb5864b8d8494fe5d0fdeab263e12f5bab7318cc788"},
{file = "zxcvbn_rs_py-0.1.1-cp37-none-win_amd64.whl", hash = "sha256:76f99daff842e13bd89af407708a0ba4f355461a9845d358a31f646dfa859dc7"},
{file = "zxcvbn_rs_py-0.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:865a4d409b27fbff8168ecaa2b729c2a3213352d1b7a9041710273a117e6ec03"},
{file = "zxcvbn_rs_py-0.1.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:600911814dff1fe5cb264ef6a340280e9adf4cef82d1b6e2b7c610ee2c3be3c1"},
{file = "zxcvbn_rs_py-0.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3861c3e6ddc8d24a38c4c887229b211b0f39207a8560c9a1437290989bc7f623"},
{file = "zxcvbn_rs_py-0.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fafe3c3172ba76f08d50005a52f8a372be08a95f44bdf4e633c0cf5dc3967de"},
{file = "zxcvbn_rs_py-0.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fd2f02d6cc567f3b0f6458a6828b736de6f6eee2a15f1937a4cc3ab4ac0b9ce"},
{file = "zxcvbn_rs_py-0.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48062e952fbbe226b1c093c641dbb8118490f8746fb005bec720a6cabd372222"},
{file = "zxcvbn_rs_py-0.1.1-cp38-none-win32.whl", hash = "sha256:c4446dfa9483007e777dee191ae92d453af5f819a9a9eacecaa6230f2a8d278a"},
{file = "zxcvbn_rs_py-0.1.1-cp38-none-win_amd64.whl", hash = "sha256:557344aed8d7b275d3745373612610333fc253fc67d8908f16357216f1f5d175"},
{file = "zxcvbn_rs_py-0.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebc72038af96cc16258eee5b7528078236b15f29c83cf31f293721a16d81a633"},
{file = "zxcvbn_rs_py-0.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a4e81616ea67bec0398260fe6290b5a8f7c5112dc5c45a0b060c744e31d4fdb"},
{file = "zxcvbn_rs_py-0.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05ba99a9ccb62da78bdf34e1e24a708edc7976bfb699f69d1fb271899846af5"},
{file = "zxcvbn_rs_py-0.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb11d14958f28c3e3f5320089d1c442083024888492dba8b16dd7a7e597482ef"},
{file = "zxcvbn_rs_py-0.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b83a5a743b4404b42b5d25aa538c7133356483f95217929f0f83dabd9b0606db"},
{file = "zxcvbn_rs_py-0.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc238a9af18d8e9009e7c391d48afa056ce08fb4da67bf739bb08bcc366c22c2"},
{file = "zxcvbn_rs_py-0.1.1-cp39-none-win32.whl", hash = "sha256:48a0a7ac6da7d929f1535b04ea7a6a5c1ad3c2089b60b63422a9a516389a9c13"},
{file = "zxcvbn_rs_py-0.1.1-cp39-none-win_amd64.whl", hash = "sha256:9756d2ec94ac8571051304a1b0a6d58496e8f3997d89565bae51ad61193bfb5b"},
{file = "zxcvbn_rs_py-0.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b9bfd147cd678c4cd4567f3758d47bdd965e1337c8e4260b6ccffd75db905b5"},
{file = "zxcvbn_rs_py-0.1.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10f1fc4c246f7abeeeff5951655a8b8f694d2793210625d6d66dc877646f3bfd"},
{file = "zxcvbn_rs_py-0.1.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5eef1ed5dd7cf5c8c687fc2c10562fa4649e03b1c0bff493ade7eea26f1ea5d"},
{file = "zxcvbn_rs_py-0.1.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b79c6241d341082add22dc869d31dbcc4afa14fd9607f6ffac2431144602653"},
{file = "zxcvbn_rs_py-0.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f194b37fc82133b6b2d9e5ed12d2de4084707cdd8cfa5cbf22836e88d9d58e"},
{file = "zxcvbn_rs_py-0.1.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d21b03ad410a4e1980a70269e534f23048af5c40a507c59b6fac90b0a72d374f"},
{file = "zxcvbn_rs_py-0.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1207ff8367c8f2bad1b12973775f739ef651a20cda23527e2a647b3d8d4ac88a"},
{file = "zxcvbn_rs_py-0.1.1-pp37-pypy37_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa34e5c817f76f908dea2f3de4b312436f23ef862892c1bf3cbd9f7c6076af08"},
{file = "zxcvbn_rs_py-0.1.1-pp37-pypy37_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af633d2ff881a36af5469884f9aa9605363485f07bb1125d2853f83ae99bc558"},
{file = "zxcvbn_rs_py-0.1.1-pp37-pypy37_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:51dde7e0b4534b3eff7870677983dbd1382ae5d17defc4ae1d9ef3248da1777b"},
{file = "zxcvbn_rs_py-0.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645ce91c85bde350e1bb8fd5e00c687d78e6f5809e8aeb8ff20e6843b729090f"},
{file = "zxcvbn_rs_py-0.1.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0d19283743e1ed9ade89cb2f74c5b8baf6450e8ca427cd0ebf242615edeb345f"},
{file = "zxcvbn_rs_py-0.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6516ba5f1b18b532fca1eb4b63b7dec11197e0f3da7bdde946ea4bba4acf80ae"},
{file = "zxcvbn_rs_py-0.1.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:febae3939795bdad83925c5f974e17d9435e03ff6ef0cf5050c7c88cd40d3f9b"},
{file = "zxcvbn_rs_py-0.1.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe9ac157fe179a7a5d0d57b028ea249ca703d4a105c577afc8585d3bc94ef3d8"},
{file = "zxcvbn_rs_py-0.1.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e442a9cd779d6abfa0de821ac017cd44c6110b13fc86e764804c44dbed0ad846"},
{file = "zxcvbn_rs_py-0.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04fb2ffd3770c28a3bf500579e1925862f9f88fa3d287ac6b871e3d07723d585"},
{file = "zxcvbn_rs_py-0.1.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e075c00b2e56db6dc2e01104a57a0dc8430a66ec8d9e50a4ac608461a9422af"},
{file = "zxcvbn_rs_py-0.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc51924ac5200084401946c4dffab86c7a7242a84a68b202aa449eb59f12ce"},
{file = "zxcvbn_rs_py-0.1.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e196bdc69697afef49d0317a9c3491bbad657047855b785d056fbf229845936"},
{file = "zxcvbn_rs_py-0.1.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0079f6704d13471db7401c4ff5f7c7255a815156ca112554c71d7f7c190f4441"},
{file = "zxcvbn_rs_py-0.1.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a5afd57b654d1a2d1ebb0a8e177183b42b52e7b539d6f4f867970eec58beecc"},
{file = "zxcvbn_rs_py-0.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2c1be214b012840fc45aed2a8eb2b43bba867322f8930d597ea40749bbb3fd"},
{file = "zxcvbn_rs_py-0.1.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:553a8be92870e0dafd2855b634f81f09208142b17e5e499943a6e83c534e749e"},
{file = "zxcvbn_rs_py-0.1.1.tar.gz", hash = "sha256:ec4649fd619e91fb278aca93b3d770c1b4226ba3a50c4c77311f6692a488fa00"},
]
[extras]
all = ["authlib", "email_validator", "flask-babel", "flask-themer", "passlib", "pycountry", "python-ldap", "pytz", "sentry-sdk", "sqlalchemy", "sqlalchemy-json", "sqlalchemy-utils", "toml"]
front = ["email_validator", "flask-babel", "flask-themer", "pycountry", "pytz", "toml"]
all = ["authlib", "email_validator", "flask-babel", "flask-themer", "passlib", "pycountry", "python-ldap", "pytz", "sentry-sdk", "sqlalchemy", "sqlalchemy-json", "sqlalchemy-utils", "toml", "zxcvbn-rs-py"]
front = ["email_validator", "flask-babel", "flask-themer", "pycountry", "pytz", "toml", "zxcvbn-rs-py"]
ldap = ["python-ldap"]
oidc = ["authlib"]
sentry = ["sentry-sdk"]
@ -2540,5 +2611,5 @@ sql = ["passlib", "sqlalchemy", "sqlalchemy-json", "sqlalchemy-utils"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "4b4e4ce2d7a8d6d586177e74b67b3e1fcd744f33f36701b7d64b2994f10396f6"
python-versions = "<3.13,>=3.10"
content-hash = "6918a9fca20831192025f6d512c1c6ed567a76b1c908496788edd2d727d3d05d"

View file

@ -36,7 +36,7 @@ readme = "README.md"
include = ["canaille/translations/*/LC_MESSAGES/*.mo"]
[tool.poetry.dependencies]
python = "^3.10"
python = "<3.13,>=3.10"
flask = "^3.0.0"
flask-wtf = "^1.2.1"
pydantic-settings = "^2.0.3"
@ -49,6 +49,7 @@ flask-themer = {version = "^2.0.0", optional=true}
pycountry = {version = ">=22.1.10", optional=true}
pytz = {version = ">=2022.7", optional=true}
toml = {version = "^0.10.0", optional=true, python = "<3.11"}
zxcvbn-rs-py = {version = "^0.1.1", optional=true}
# extra : oidc
authlib = {version = "^1.2.1", optional=true}
@ -119,6 +120,7 @@ front = [
"pycountry",
"pytz",
"toml",
"zxcvbn-rs-py",
]
ldap = [
"python-ldap",
@ -150,6 +152,7 @@ all = [
"sqlalchemy",
"sqlalchemy-json",
"sqlalchemy-utils",
"zxcvbn-rs-py",
]
[tool.poetry.scripts]

View file

@ -7,6 +7,8 @@ from flask import current_app
from werkzeug.datastructures import ImmutableMultiDict
from canaille.app.forms import DateTimeUTCField
from canaille.app.forms import password_length_validator
from canaille.app.forms import password_too_long_validator
from canaille.app.forms import phone_number
@ -259,3 +261,75 @@ def test_phone_number_validator():
with pytest.raises(wtforms.ValidationError):
phone_number(None, Field("invalid"))
def test_minimum_password_length_config(testclient):
class Field:
def __init__(self, data):
self.data = data
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = 20
password_length_validator(None, Field("12345678901234567890"))
with pytest.raises(wtforms.ValidationError):
password_length_validator(None, Field("1234567890123456789"))
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = 8
password_length_validator(None, Field("12345678"))
with pytest.raises(wtforms.ValidationError):
password_length_validator(None, Field("1234567"))
with pytest.raises(wtforms.ValidationError):
password_length_validator(None, Field("1"))
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = 0
password_length_validator(None, Field(""))
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = None
password_length_validator(None, Field(""))
def test_password_strength_progress_bar(testclient, logged_user):
res = testclient.get("/profile/user/settings")
res = testclient.post(
"/profile/user/settings",
{
"csrf_token": res.form["csrf_token"].value,
"password1": "new_password",
},
headers={
"HX-Request": "true",
"HX-Trigger-Name": "password1",
},
)
res.mustcontain('data-percent="50"')
def test_maximum_password_length_config(testclient):
class Field:
def __init__(self, data):
self.data = data
password_too_long_validator(None, Field("a" * 1000))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 1001))
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = 500
password_too_long_validator(None, Field("a" * 500))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 501))
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = None
password_too_long_validator(None, Field("a" * 4096))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 4097))
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = 0
password_too_long_validator(None, Field("a" * 4096))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 4097))
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = 5000
password_too_long_validator(None, Field("a" * 4096))
with pytest.raises(wtforms.ValidationError):
password_too_long_validator(None, Field("a" * 4097))

View file

@ -2,6 +2,7 @@ import datetime
import logging
from unittest import mock
from flask import current_app
from flask import g
from canaille.app import models
@ -97,6 +98,64 @@ def test_profile_settings_edition_dynamic_validation(testclient, logged_admin):
res.mustcontain("Field must be at least 8 characters long.")
def test_profile_settings_minimum_password_length_validation(testclient, logged_user):
"""Tests minimum length of password defined in configuration."""
def with_different_values(password, length):
current_app.config["CANAILLE"]["MIN_PASSWORD_LENGTH"] = length
res = testclient.get("/profile/user/settings")
res = testclient.post(
"/profile/user/settings",
{
"csrf_token": res.form["csrf_token"].value,
"password1": password,
},
headers={
"HX-Request": "true",
"HX-Trigger-Name": "password1",
},
)
res.mustcontain(f"Field must be at least {length} characters long.")
with_different_values("short", 8)
with_different_values("aa", 3)
with_different_values("1234567890123456789", 20)
def test_profile_settings_too_long_password(testclient, logged_user):
"""Tests maximum length of password."""
def with_different_values(password, length, message):
current_app.config["CANAILLE"]["MAX_PASSWORD_LENGTH"] = length
res = testclient.get("/profile/user/settings")
res = testclient.post(
"/profile/user/settings",
{
"csrf_token": res.form["csrf_token"].value,
"password1": password,
},
headers={
"HX-Request": "true",
"HX-Trigger-Name": "password1",
},
)
res.mustcontain(message)
with_different_values(
"a" * 1001, 1000, "Field cannot be longer than 1000 characters."
)
with_different_values("a1!A" * 250, 1000, 'data-percent="25"')
with_different_values("a" * 501, 500, "Field cannot be longer than 500 characters.")
with_different_values("a1!A" * 125, 500, 'data-percent="25"')
with_different_values("a" * 4097, 0, "Field cannot be longer than 4096 characters.")
with_different_values(
"a" * 4097, None, "Field cannot be longer than 4096 characters."
)
with_different_values(
"a" * 4097, 5000, "Field cannot be longer than 4096 characters."
)
def test_edition_without_groups(
testclient,
logged_user,