forked from Github-Mirrors/canaille
Merge branch 'password-strength' into 'main'
Password strength See merge request yaal/canaille!182
This commit is contained in:
commit
05cc09ab74
13 changed files with 399 additions and 95 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
115
poetry.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue