forked from Github-Mirrors/canaille
Merge branch '179-check-passwords-on-compromised-password-databases' of gitlab.com:yaal/canaille into 179-check-passwords-on-compromised-password-databases
This commit is contained in:
commit
faa5c6f966
18 changed files with 857 additions and 136 deletions
|
@ -1,3 +1,10 @@
|
|||
[0.0.57] - Unreleased
|
||||
---------------------
|
||||
|
||||
Added
|
||||
^^^^^
|
||||
- Password compromise check :issue:`179`
|
||||
|
||||
[0.0.56] - 2024-11-07
|
||||
---------------------
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import datetime
|
||||
import math
|
||||
import re
|
||||
from hashlib import sha1
|
||||
|
||||
import requests
|
||||
import wtforms.validators
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
|
@ -15,6 +17,7 @@ from canaille.app.i18n import DEFAULT_LANGUAGE_CODE
|
|||
from canaille.app.i18n import gettext as _
|
||||
from canaille.app.i18n import locale_selector
|
||||
from canaille.app.i18n import timezone_selector
|
||||
from canaille.app.mails_sending_conditions import check_if_send_mail_to_admins
|
||||
from canaille.backends import Backend
|
||||
|
||||
from . import validate_uri
|
||||
|
@ -84,6 +87,28 @@ def password_strength_calculator(password):
|
|||
return strength_score
|
||||
|
||||
|
||||
def compromised_password_validator(form, field):
|
||||
hashed_password = sha1(field.data.encode("utf-8")).hexdigest()
|
||||
hashed_password_prefix, hashed_password_suffix = (
|
||||
hashed_password[:5].upper(),
|
||||
hashed_password[5:].upper(),
|
||||
)
|
||||
|
||||
api_url = f"https://api.pwnedpasswords.com/range/{hashed_password_prefix}"
|
||||
|
||||
try:
|
||||
response = requests.api.get(api_url, timeout=10)
|
||||
except Exception:
|
||||
check_if_send_mail_to_admins(form, api_url, hashed_password_suffix)
|
||||
return None
|
||||
|
||||
decoded_response = response.content.decode("utf8").split("\r\n")
|
||||
|
||||
for each in decoded_response:
|
||||
if hashed_password_suffix == each.split(":")[0]:
|
||||
raise wtforms.ValidationError(_("This password is compromised."))
|
||||
|
||||
|
||||
def email_validator(form, field):
|
||||
try:
|
||||
import email_validator # noqa: F401
|
||||
|
|
81
canaille/app/mails_sending_conditions.py
Normal file
81
canaille/app/mails_sending_conditions.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
from flask import current_app
|
||||
from flask import flash
|
||||
|
||||
from canaille.app import models
|
||||
from canaille.app.i18n import gettext as _
|
||||
from canaille.backends import Backend
|
||||
from canaille.core.mails import send_compromised_password_check_failure_mail
|
||||
|
||||
from .flask import request_is_htmx
|
||||
|
||||
|
||||
def check_if_send_mail_to_admins(form, api_url, hashed_password_suffix):
|
||||
if current_app.features.has_smtp and not request_is_htmx():
|
||||
flash(
|
||||
_(
|
||||
"Password compromise investigation failed. "
|
||||
"Please contact the administrators."
|
||||
),
|
||||
"error",
|
||||
)
|
||||
|
||||
group_user = Backend.instance.query(models.User)
|
||||
|
||||
if (
|
||||
current_app.config["CANAILLE"]["ACL"]
|
||||
and current_app.config["CANAILLE"]["ACL"]["ADMIN"]
|
||||
and current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"]
|
||||
and current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"]["groups"]
|
||||
):
|
||||
admin_group_display_name = current_app.config["CANAILLE"]["ACL"]["ADMIN"][
|
||||
"FILTER"
|
||||
]["groups"]
|
||||
|
||||
admin_emails = [
|
||||
user.emails[0]
|
||||
for user in group_user
|
||||
if any(
|
||||
group.display_name == admin_group_display_name
|
||||
for group in user.groups
|
||||
)
|
||||
]
|
||||
else:
|
||||
admin_emails = [current_app.config["CANAILLE"]["ADMIN_EMAIL"]]
|
||||
|
||||
if form.user is not None:
|
||||
user_name = form.user.user_name
|
||||
user_email = form.user.emails[0]
|
||||
else:
|
||||
user_name = form["user_name"].data
|
||||
user_email = form["emails"].data[0]
|
||||
|
||||
number_emails_send = 0
|
||||
|
||||
for email in admin_emails:
|
||||
if send_compromised_password_check_failure_mail(
|
||||
api_url, user_name, user_email, hashed_password_suffix, email
|
||||
):
|
||||
number_emails_send += 1
|
||||
else:
|
||||
pass
|
||||
|
||||
if number_emails_send > 0:
|
||||
flash(
|
||||
_(
|
||||
"We have informed your administrator about the failure of the password compromise investigation."
|
||||
),
|
||||
"success",
|
||||
)
|
||||
else:
|
||||
flash(
|
||||
_(
|
||||
"An error occurred while communicating the incident to the administrators. "
|
||||
"Please update your password as soon as possible. "
|
||||
"If this still happens, please contact the administrators."
|
||||
),
|
||||
"error",
|
||||
)
|
||||
return None
|
||||
|
||||
return number_emails_send
|
||||
return None
|
|
@ -312,3 +312,11 @@ class CoreSettings(BaseModel):
|
|||
characters. If the value entered is 0 or None, or greater than 4096,
|
||||
then 4096 will be retained.
|
||||
"""
|
||||
|
||||
ADMIN_EMAIL: str = None
|
||||
"""Administration email contact.
|
||||
|
||||
In certain special cases (example : questioning about password
|
||||
corruption), it is necessary to provide an administration contact
|
||||
email.
|
||||
"""
|
||||
|
|
|
@ -31,6 +31,7 @@ from canaille.app.flask import smtp_needed
|
|||
from canaille.app.flask import user_needed
|
||||
from canaille.app.forms import IDToModel
|
||||
from canaille.app.forms import TableForm
|
||||
from canaille.app.forms import compromised_password_validator
|
||||
from canaille.app.forms import is_readonly
|
||||
from canaille.app.forms import password_length_validator
|
||||
from canaille.app.forms import password_too_long_validator
|
||||
|
@ -316,6 +317,7 @@ def registration(data=None, hash=None):
|
|||
wtforms.validators.DataRequired(),
|
||||
password_length_validator,
|
||||
password_too_long_validator,
|
||||
compromised_password_validator,
|
||||
]
|
||||
form["password2"].validators = [
|
||||
wtforms.validators.DataRequired(),
|
||||
|
|
|
@ -10,6 +10,7 @@ from canaille.app.forms import BaseForm
|
|||
from canaille.app.forms import DateTimeUTCField
|
||||
from canaille.app.forms import Form
|
||||
from canaille.app.forms import IDToModel
|
||||
from canaille.app.forms import compromised_password_validator
|
||||
from canaille.app.forms import email_validator
|
||||
from canaille.app.forms import is_uri
|
||||
from canaille.app.forms import password_length_validator
|
||||
|
@ -265,6 +266,7 @@ PROFILE_FORM_FIELDS = dict(
|
|||
wtforms.validators.Optional(),
|
||||
password_length_validator,
|
||||
password_too_long_validator,
|
||||
compromised_password_validator,
|
||||
],
|
||||
render_kw={
|
||||
"autocomplete": "new-password",
|
||||
|
|
|
@ -210,3 +210,42 @@ def send_registration_mail(email, registration_url):
|
|||
html=html_body,
|
||||
attachments=[(logo_cid, logo_filename, logo_raw)] if logo_filename else None,
|
||||
)
|
||||
|
||||
|
||||
def send_compromised_password_check_failure_mail(
|
||||
check_password_url, user_name, user_email, hashed_password, admin_email
|
||||
):
|
||||
base_url = url_for("core.account.index", _external=True)
|
||||
logo_cid, logo_filename, logo_raw = logo()
|
||||
|
||||
subject = _("compromised password check failure on {website_name}").format(
|
||||
website_name=current_app.config["CANAILLE"]["NAME"]
|
||||
)
|
||||
text_body = render_template(
|
||||
"mails/compromised_password_check_failure.txt",
|
||||
site_name=current_app.config["CANAILLE"]["NAME"],
|
||||
site_url=base_url,
|
||||
check_password_url=check_password_url,
|
||||
user_name=user_name,
|
||||
user_email=user_email,
|
||||
hashed_password=hashed_password,
|
||||
)
|
||||
html_body = render_template(
|
||||
"mails/compromised_password_check_failure.html",
|
||||
site_name=current_app.config["CANAILLE"]["NAME"],
|
||||
site_url=base_url,
|
||||
check_password_url=check_password_url,
|
||||
logo=f"cid:{logo_cid[1:-1]}" if logo_cid else None,
|
||||
title=subject,
|
||||
user_name=user_name,
|
||||
user_email=user_email,
|
||||
hashed_password=hashed_password,
|
||||
)
|
||||
|
||||
return send_email(
|
||||
subject=subject,
|
||||
recipient=admin_email,
|
||||
text=text_body,
|
||||
html=html_body,
|
||||
attachments=[(logo_cid, logo_filename, logo_raw)] if logo_filename else None,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="https://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<style type="text/css" style="font-weight: 300">@import url({{ url_for('static', filename='fonts/lato.css', _external=True) }});</style>
|
||||
<title>{{ title }}</title>
|
||||
</head>
|
||||
<body style="color: rgba(0,0,0,.87); padding: 1em; margin: auto; width: 700px; font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; font-weight: 400; font-size: 14px;">
|
||||
|
||||
<table cellspacing="0" cellpadding="0" border="0" style="font-weight: 400; background: #fff; font-size: 1rem; margin-top: 0; margin-bottom: 0; width: 700px;">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<h3 style="font-weight: 700; line-height: 1.3em; font-size: 1.3rem; padding: .8rem 1rem; margin: 0; box-shadow: none; border: 1px solid #d4d4d5; border-radius: .3rem .3rem 0 0;">
|
||||
{% if logo %}
|
||||
<img src="{{ logo }}" alt="{{ site_name }}" style="font-size: 1.3rem; border-style: none; width: 50px; display: inline-block; margin-top: .14em; vertical-align: middle;">
|
||||
{% endif %}
|
||||
<div style="font-size: 1.3rem; display: inline-block; padding-left: .75rem; vertical-align: middle;">
|
||||
{% trans %}Compromised password check failure{% endtrans %}
|
||||
</div>
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" style="background: #f8f8f9; padding: 1em 1.5em; line-height: 1.4em; font-size: 1em; margin: 0; border-radius: 0; text-align: justify; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5;">
|
||||
<p>
|
||||
{% trans %}Our services were unable to verify if the {{ user_name }}'s password is compromised.{% endtrans %}</br>
|
||||
</p>
|
||||
<p>
|
||||
{% trans %}You have to check manually if the new password of the user {{ user_name }} is compromised.{% endtrans %}</br>
|
||||
{% trans %}Follow this steps : {% endtrans %}</br>
|
||||
{% trans %}1. click on the link above "Check if password is compromised".{% endtrans %}</br>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="margin: 0; border-radius: 0; text-align:center; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5; border-bottom: 1px solid #d4d4d5;">
|
||||
<td colspan="2" style="background: #2185d0; width:50%; border-radius: 0 0 .3rem 0;">
|
||||
<a href="{{ check_password_url }}" style="width: 100%; display: inline-block; vertical-align: middle; padding: .8em 0; font-weight: 700; line-height: 1em; text-align: center; text-decoration: none; font-size: 1rem; color: #fff; margin: 0; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{% trans %}Check if password is compromised{% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" style="background: #f8f8f9; padding: 1em 1.5em; line-height: 1.4em; font-size: 1em; margin: 0; border-radius: 0; text-align: justify; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5;">
|
||||
<p>
|
||||
{% trans %}2. in the page that will open, search the following hashed password in the page : {{ hashed_password }}{% endtrans %}</br>
|
||||
{% trans %}3. if the password is in the list :{% endtrans %}</br>
|
||||
{% trans %}3.1. open this link and reset user's password. {% endtrans %}</br>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr style="margin: 0; border-radius: 0; text-align:center; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5; border-bottom: 1px solid #d4d4d5;">
|
||||
<td colspan="2" style="background: #2185d0; width:50%; border-radius: 0 0 .3rem 0;">
|
||||
<a href="http://127.0.0.1:5000/profile/{{ user_name }}/settings" style="width: 100%; display: inline-block; vertical-align: middle; padding: .8em 0; font-weight: 700; line-height: 1em; text-align: center; text-decoration: none; font-size: 1rem; color: #fff; margin: 0; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{% trans %} Reset {{ user_name }}'s password {% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" style="background: #f8f8f9; padding: 1em 1.5em; line-height: 1.4em; font-size: 1em; margin: 0; border-radius: 0; text-align: justify; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5;">
|
||||
<p>
|
||||
{% trans %}3.2. send an email to the user to explain the situation : {{ user_email }}{% endtrans %}</br>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr style="margin: 0; border-radius: 0; text-align:center; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5; border-bottom: 1px solid #d4d4d5;">
|
||||
<td style="background: #e0e1e2; width:50%; border-radius: 0 0 0 .3rem;">
|
||||
<a href="{{ site_url }}" style="width: 100%; display: inline-block; vertical-align: middle; color: rgba(0,0,0,.6); padding: .8em 0; font-weight: 700; line-height: 1em; text-align: center; text-decoration: none; font-size: 1rem;margin: 0; border-left: none; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{{ site_name }}</a>
|
||||
</td>
|
||||
<td style="background: #2185d0; width:50%; border-radius: 0 0 .3rem 0;">
|
||||
<a href="{{ check_password_url }}" style="width: 100%; display: inline-block; vertical-align: middle; padding: .8em 0; font-weight: 700; line-height: 1em; text-align: center; text-decoration: none; font-size: 1rem; color: #fff; margin: 0; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{% trans %}Check if password is compromised{% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,14 @@
|
|||
# {% trans %}Compromised password check failure{% endtrans %}
|
||||
|
||||
{% trans %}Our services were unable to verify if the http://127.0.0.1:5000/profile/{{ user_name }}/settings's password is compromised.{% endtrans %}
|
||||
|
||||
{% trans %}You have to check manually if the new password of the user {{ user_name }} is compromised.{% endtrans %}
|
||||
{% trans %}Follow this steps : {% endtrans %}
|
||||
{% trans %}1. click on the link above "Check if password is compromised".{% endtrans %}
|
||||
{% trans %}2. in the page that will open, search the following hashed password in the page : {{ hashed_password }}{% endtrans %}
|
||||
{% trans %}3. if the password is in the list :{% endtrans %}
|
||||
{% trans %}3.1. open this link http://127.0.0.1:5000/profile/{{ user_name }}/settings and reset user's password.{% endtrans %}
|
||||
{% trans %}3.2. send an email to the user to explain the situation : {{ user_email }}.{% endtrans %}
|
||||
|
||||
{% trans %}Check if password is compromised{% endtrans %}: {{ check_password_url }}
|
||||
{{ site_name }}: {{ site_url }}
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2024-10-28 11:57+0100\n"
|
||||
"POT-Creation-Date: 2024-11-12 16:52+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"
|
||||
|
@ -17,38 +17,61 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.16.0\n"
|
||||
|
||||
#: canaille/app/flask.py:100
|
||||
#: canaille/app/flask.py:53
|
||||
msgid "No SMTP server has been configured"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/forms.py:27
|
||||
#: canaille/app/forms.py:29
|
||||
msgid "This is not a valid URL"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/forms.py:34 canaille/app/forms.py:35
|
||||
#: canaille/app/forms.py:36 canaille/app/forms.py:37
|
||||
msgid "This value is a duplicate"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/forms.py:47
|
||||
#: canaille/app/forms.py:49
|
||||
msgid "Not a valid phone number"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/forms.py:55
|
||||
#: canaille/app/forms.py:57
|
||||
msgid "Field must be at least {minimum_password_length} characters long."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/forms.py:67
|
||||
#: canaille/app/forms.py:69
|
||||
msgid "Field cannot be longer than {maximum_password_length} characters."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/forms.py:240
|
||||
#: canaille/app/forms.py:109
|
||||
msgid "This password is compromised."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/forms.py:269
|
||||
msgid "The page number is not valid"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/forms.py:268
|
||||
#: canaille/app/forms.py:297
|
||||
msgid "Not a valid datetime value."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/mails_sending_conditions.py:15
|
||||
msgid ""
|
||||
"Password compromise investigation failed. Please contact the "
|
||||
"administrators."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/mails_sending_conditions.py:64
|
||||
msgid ""
|
||||
"We have informed your administrator about the failure of the password "
|
||||
"compromise investigation."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/app/mails_sending_conditions.py:71
|
||||
msgid ""
|
||||
"An error occurred while communicating the incident to the administrators."
|
||||
" Please update your password as soon as possible. If this still happens, "
|
||||
"please contact the administrators."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/backends/ldap/backend.py:99
|
||||
msgid "Could not connect to the LDAP server '{uri}'"
|
||||
msgstr ""
|
||||
|
@ -61,8 +84,8 @@ msgstr ""
|
|||
msgid "John Doe"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/backends/ldap/backend.py:178 canaille/core/endpoints/forms.py:164
|
||||
#: canaille/core/endpoints/forms.py:424
|
||||
#: canaille/backends/ldap/backend.py:178 canaille/core/endpoints/forms.py:165
|
||||
#: canaille/core/endpoints/forms.py:426
|
||||
msgid "jdoe"
|
||||
msgstr ""
|
||||
|
||||
|
@ -108,36 +131,40 @@ msgstr ""
|
|||
msgid "Continue your registration on {website_name}"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:91 canaille/core/endpoints/account.py:117
|
||||
#: canaille/core/mails.py:221
|
||||
msgid "compromised password check failure on {website_name}"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:92 canaille/core/endpoints/account.py:118
|
||||
msgid "You will receive soon an email to continue the registration process."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:124
|
||||
#: canaille/core/endpoints/account.py:125
|
||||
msgid ""
|
||||
"An error happened while sending your registration mail. Please try again "
|
||||
"in a few minutes. If this still happens, please contact the "
|
||||
"administrators."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:248
|
||||
#: canaille/core/endpoints/account.py:271
|
||||
#: canaille/core/endpoints/account.py:249
|
||||
#: canaille/core/endpoints/account.py:272
|
||||
msgid "The registration link that brought you here was invalid."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:255
|
||||
#: canaille/core/endpoints/account.py:256
|
||||
msgid "The registration link that brought you here has expired."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:264
|
||||
#: canaille/core/endpoints/account.py:265
|
||||
msgid "Your account has already been created."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:278
|
||||
#: canaille/core/endpoints/account.py:279
|
||||
msgid "You are already logged in, you cannot create an account."
|
||||
msgstr ""
|
||||
|
||||
#: 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/endpoints/account.py:300 canaille/core/endpoints/forms.py:316
|
||||
#: canaille/core/endpoints/forms.py:444 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
|
||||
|
@ -145,114 +172,114 @@ msgstr ""
|
|||
msgid "Groups"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:336
|
||||
#: canaille/core/endpoints/account.py:429
|
||||
#: canaille/core/endpoints/account.py:338
|
||||
#: canaille/core/endpoints/account.py:431
|
||||
msgid "User account creation failed."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:347
|
||||
#: canaille/core/endpoints/account.py:349
|
||||
msgid "Your account has been created successfully."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:362
|
||||
#: canaille/core/endpoints/account.py:384
|
||||
#: canaille/core/endpoints/account.py:364
|
||||
#: canaille/core/endpoints/account.py:386
|
||||
msgid "The email confirmation link that brought you here is invalid."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:369
|
||||
#: canaille/core/endpoints/account.py:371
|
||||
msgid "The email confirmation link that brought you here has expired."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:376
|
||||
#: canaille/core/endpoints/account.py:378
|
||||
msgid "The invitation link that brought you here was invalid."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:391
|
||||
#: canaille/core/endpoints/account.py:393
|
||||
msgid "This address email have already been confirmed."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:398
|
||||
#: canaille/core/endpoints/account.py:400
|
||||
msgid "This address email is already associated with another account."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:405
|
||||
#: canaille/core/endpoints/account.py:407
|
||||
msgid "Your email address have been confirmed."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:439
|
||||
#: canaille/core/endpoints/account.py:441
|
||||
msgid "User account creation succeed."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:615
|
||||
#: canaille/core/endpoints/account.py:784
|
||||
#: canaille/core/endpoints/account.py:617
|
||||
#: canaille/core/endpoints/account.py:786
|
||||
msgid "Profile edition failed."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:625
|
||||
#: canaille/core/endpoints/account.py:802
|
||||
#: canaille/core/endpoints/account.py:627
|
||||
#: canaille/core/endpoints/account.py:804
|
||||
msgid "Profile updated successfully."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:633
|
||||
#: canaille/core/endpoints/account.py:635
|
||||
msgid "Email addition failed."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:638
|
||||
#: canaille/core/endpoints/account.py:640
|
||||
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:645
|
||||
#: canaille/core/endpoints/account.py:647
|
||||
msgid "Could not send the verification email"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:655
|
||||
#: canaille/core/endpoints/account.py:657
|
||||
msgid "Email deletion failed."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:658
|
||||
#: canaille/core/endpoints/account.py:660
|
||||
msgid "The email have been successfully deleted."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:695
|
||||
#: canaille/core/endpoints/account.py:697
|
||||
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:702 canaille/core/endpoints/auth.py:159
|
||||
#: canaille/core/endpoints/account.py:704 canaille/core/endpoints/auth.py:159
|
||||
msgid "Could not send the password initialization email"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:713
|
||||
#: canaille/core/endpoints/account.py:715
|
||||
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:720
|
||||
#: canaille/core/endpoints/account.py:722
|
||||
msgid "Could not send the password reset email"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:736
|
||||
#: canaille/core/endpoints/account.py:738
|
||||
msgid "The account has been locked"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:747
|
||||
#: canaille/core/endpoints/account.py:749
|
||||
msgid "The account has been unlocked"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:822
|
||||
#: canaille/core/endpoints/account.py:824
|
||||
#, python-format
|
||||
msgid "The user %(user)s has been successfully deleted"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:839
|
||||
#: canaille/core/endpoints/account.py:841
|
||||
msgid "Locked users cannot be impersonated."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/account.py:843 canaille/core/endpoints/auth.py:112
|
||||
#: canaille/core/endpoints/account.py:845 canaille/core/endpoints/auth.py:112
|
||||
#, python-format
|
||||
msgid "Connection successful. Welcome %(user)s"
|
||||
msgstr ""
|
||||
|
@ -262,10 +289,10 @@ msgstr ""
|
|||
msgid "Email"
|
||||
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:410 canaille/core/endpoints/forms.py:436
|
||||
#: canaille/core/endpoints/forms.py:460 canaille/core/endpoints/forms.py:476
|
||||
#: canaille/core/endpoints/admin.py:29 canaille/core/endpoints/forms.py:98
|
||||
#: canaille/core/endpoints/forms.py:121 canaille/core/endpoints/forms.py:210
|
||||
#: canaille/core/endpoints/forms.py:412 canaille/core/endpoints/forms.py:438
|
||||
#: canaille/core/endpoints/forms.py:462 canaille/core/endpoints/forms.py:478
|
||||
msgid "jane@doe.com"
|
||||
msgstr ""
|
||||
|
||||
|
@ -334,66 +361,66 @@ msgstr ""
|
|||
msgid "Your password has been updated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:31
|
||||
#: canaille/core/endpoints/forms.py:32
|
||||
msgid "The user name '{user_name}' already exists"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:40
|
||||
#: canaille/core/endpoints/forms.py:41
|
||||
msgid "The email '{email}' is already used"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:47
|
||||
#: canaille/core/endpoints/forms.py:48
|
||||
msgid "The group '{group}' already exists"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:56
|
||||
#: canaille/core/endpoints/forms.py:57
|
||||
msgid "The login '{login}' does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:63
|
||||
#: canaille/core/endpoints/forms.py:64
|
||||
msgid "The user you are trying to remove does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:68
|
||||
#: canaille/core/endpoints/forms.py:69
|
||||
msgid "The user '{user}' has already been removed from the group '{group}'"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:86
|
||||
#: canaille/core/endpoints/forms.py:87
|
||||
msgid ""
|
||||
"The group '{group}' cannot be removed, because it must have at least one "
|
||||
"user left."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:94 canaille/core/endpoints/forms.py:117
|
||||
#: canaille/core/endpoints/forms.py:95 canaille/core/endpoints/forms.py:118
|
||||
#: canaille/core/templates/partial/group-members.html:9
|
||||
#: canaille/core/templates/partial/users.html:9
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:107 canaille/core/endpoints/forms.py:129
|
||||
#: canaille/core/endpoints/forms.py:263
|
||||
#: canaille/core/endpoints/forms.py:108 canaille/core/endpoints/forms.py:130
|
||||
#: canaille/core/endpoints/forms.py:264
|
||||
#: canaille/core/templates/profile_settings.html:63
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:136 canaille/core/endpoints/forms.py:274
|
||||
#: canaille/core/endpoints/forms.py:137 canaille/core/endpoints/forms.py:276
|
||||
msgid "Password confirmation"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:139 canaille/core/endpoints/forms.py:277
|
||||
#: canaille/core/endpoints/forms.py:140 canaille/core/endpoints/forms.py:279
|
||||
msgid "Password and confirmation do not match."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:158
|
||||
#: canaille/core/endpoints/forms.py:159
|
||||
msgid "Automatic"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:163
|
||||
#: canaille/core/endpoints/forms.py:164
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:167 canaille/core/endpoints/forms.py:366
|
||||
#: canaille/core/endpoints/forms.py:380
|
||||
#: canaille/core/endpoints/forms.py:168 canaille/core/endpoints/forms.py:368
|
||||
#: canaille/core/endpoints/forms.py:382
|
||||
#: canaille/core/templates/partial/group-members.html:12
|
||||
#: canaille/core/templates/partial/groups.html:6
|
||||
#: canaille/core/templates/partial/users.html:12
|
||||
|
@ -402,165 +429,165 @@ msgstr ""
|
|||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:169
|
||||
#: canaille/core/endpoints/forms.py:170
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:169
|
||||
#: canaille/core/endpoints/forms.py:170
|
||||
msgid "Vice president"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:172
|
||||
#: canaille/core/endpoints/forms.py:173
|
||||
msgid "Given name"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:174
|
||||
#: canaille/core/endpoints/forms.py:175
|
||||
msgid "John"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:180
|
||||
#: canaille/core/endpoints/forms.py:181
|
||||
msgid "Family Name"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:183
|
||||
#: canaille/core/endpoints/forms.py:184
|
||||
msgid "Doe"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:189
|
||||
#: canaille/core/endpoints/forms.py:190
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:192
|
||||
#: canaille/core/endpoints/forms.py:193
|
||||
msgid "Johnny"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:199 canaille/core/endpoints/forms.py:466
|
||||
#: canaille/core/endpoints/forms.py:200 canaille/core/endpoints/forms.py:468
|
||||
#: canaille/core/templates/profile_edit.html:176
|
||||
msgid "Email addresses"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:205 canaille/core/endpoints/forms.py:456
|
||||
#: canaille/core/endpoints/forms.py:206 canaille/core/endpoints/forms.py:458
|
||||
msgid ""
|
||||
"This email will be used as a recovery address to reset the password if "
|
||||
"needed"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:219
|
||||
#: canaille/core/endpoints/forms.py:220
|
||||
msgid "Phone numbers"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:220
|
||||
#: canaille/core/endpoints/forms.py:221
|
||||
msgid "555-000-555"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:227
|
||||
#: canaille/core/endpoints/forms.py:228
|
||||
msgid "Address"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:229
|
||||
#: canaille/core/endpoints/forms.py:230
|
||||
msgid "132, Foobar Street, Gotham City 12401, XX"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:233
|
||||
#: canaille/core/endpoints/forms.py:234
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:235
|
||||
#: canaille/core/endpoints/forms.py:236
|
||||
msgid "132, Foobar Street"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:239
|
||||
#: canaille/core/endpoints/forms.py:240
|
||||
msgid "Postal Code"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:245
|
||||
#: canaille/core/endpoints/forms.py:246
|
||||
msgid "Locality"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:247
|
||||
#: canaille/core/endpoints/forms.py:248
|
||||
msgid "Gotham City"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:251
|
||||
#: canaille/core/endpoints/forms.py:252
|
||||
msgid "Region"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:253
|
||||
#: canaille/core/endpoints/forms.py:254
|
||||
msgid "North Pole"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:257
|
||||
#: canaille/core/endpoints/forms.py:258
|
||||
msgid "Photo"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:261
|
||||
#: canaille/core/endpoints/forms.py:262
|
||||
#: canaille/core/templates/profile_add.html:56
|
||||
#: canaille/core/templates/profile_edit.html:64
|
||||
msgid "Delete the photo"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:285
|
||||
#: canaille/core/endpoints/forms.py:287
|
||||
msgid "User number"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:287 canaille/core/endpoints/forms.py:293
|
||||
#: canaille/core/endpoints/forms.py:289 canaille/core/endpoints/forms.py:295
|
||||
msgid "1234"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:291
|
||||
#: canaille/core/endpoints/forms.py:293
|
||||
msgid "Department"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:297
|
||||
#: canaille/core/endpoints/forms.py:299
|
||||
msgid "Organization"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:299
|
||||
#: canaille/core/endpoints/forms.py:301
|
||||
msgid "Cogip LTD."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:303
|
||||
#: canaille/core/endpoints/forms.py:305
|
||||
msgid "Website"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:305
|
||||
#: canaille/core/endpoints/forms.py:307
|
||||
msgid "https://mywebsite.tld"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:310
|
||||
#: canaille/core/endpoints/forms.py:312
|
||||
msgid "Preferred language"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:320
|
||||
#: canaille/core/endpoints/forms.py:322
|
||||
msgid "users, admins …"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:345
|
||||
#: canaille/core/endpoints/forms.py:347
|
||||
msgid "Account expiration"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:369
|
||||
#: canaille/core/endpoints/forms.py:371
|
||||
msgid "group"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:373 canaille/core/endpoints/forms.py:390
|
||||
#: canaille/core/endpoints/forms.py:375 canaille/core/endpoints/forms.py:392
|
||||
#: canaille/core/templates/partial/groups.html:7
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:404 canaille/core/endpoints/forms.py:429
|
||||
#: canaille/core/endpoints/forms.py:406 canaille/core/endpoints/forms.py:431
|
||||
msgid "Email address"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:423
|
||||
#: canaille/core/endpoints/forms.py:425
|
||||
msgid "User name"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:427
|
||||
#: canaille/core/endpoints/forms.py:429
|
||||
msgid "Username editable by the invitee"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/endpoints/forms.py:469
|
||||
#: canaille/core/endpoints/forms.py:471
|
||||
msgid "New email address"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1139,6 +1166,89 @@ msgstr ""
|
|||
msgid "Registration"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:19
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:1
|
||||
msgid "Compromised password check failure"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:28
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Our services were unable to verify if the %(user_name)s's password is "
|
||||
"compromised."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:31
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:5
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You have to check manually if the new password of the user %(user_name)s "
|
||||
"is compromised."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:32
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:6
|
||||
msgid "Follow this steps :"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:33
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:7
|
||||
msgid "1. click on the link above \"Check if password is compromised\"."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:40
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:75
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:13
|
||||
msgid "Check if password is compromised"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:47
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:8
|
||||
#, python-format
|
||||
msgid ""
|
||||
"2. in the page that will open, search the following hashed password in "
|
||||
"the page : %(hashed_password)s"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:48
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:9
|
||||
msgid "3. if the password is in the list :"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:49
|
||||
msgid "3.1. open this link and reset user's password."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:57
|
||||
#, python-format
|
||||
msgid "Reset %(user_name)s's password"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.html:64
|
||||
#, python-format
|
||||
msgid "3.2. send an email to the user to explain the situation : %(user_email)s"
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:3
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Our services were unable to verify if the "
|
||||
"http://127.0.0.1:5000/profile/%(user_name)s/settings's password is "
|
||||
"compromised."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:10
|
||||
#, python-format
|
||||
msgid ""
|
||||
"3.1. open this link http://127.0.0.1:5000/profile/%(user_name)s/settings "
|
||||
"and reset user's password."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/compromised_password_check_failure.txt:11
|
||||
#, python-format
|
||||
msgid "3.2. send an email to the user to explain the situation : %(user_email)s."
|
||||
msgstr ""
|
||||
|
||||
#: canaille/core/templates/mails/email-confirmation.html:19
|
||||
msgid "Email address confirmation"
|
||||
msgstr ""
|
||||
|
|
|
@ -33,6 +33,7 @@ dependencies = [
|
|||
"flask >= 3.0.0",
|
||||
"flask-wtf >= 1.2.1",
|
||||
"pydantic-settings >= 2.0.3",
|
||||
"requests>=2.32.3",
|
||||
"wtforms >= 3.1.1",
|
||||
]
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import wtforms
|
||||
|
@ -7,6 +8,7 @@ from flask import current_app
|
|||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from canaille.app.forms import DateTimeUTCField
|
||||
from canaille.app.forms import compromised_password_validator
|
||||
from canaille.app.forms import password_length_validator
|
||||
from canaille.app.forms import password_too_long_validator
|
||||
from canaille.app.forms import phone_number
|
||||
|
@ -295,14 +297,14 @@ def test_password_strength_progress_bar(testclient, logged_user):
|
|||
"/profile/user/settings",
|
||||
{
|
||||
"csrf_token": res.form["csrf_token"].value,
|
||||
"password1": "new_password",
|
||||
"password1": "i'm a little pea",
|
||||
},
|
||||
headers={
|
||||
"HX-Request": "true",
|
||||
"HX-Trigger-Name": "password1",
|
||||
},
|
||||
)
|
||||
res.mustcontain('data-percent="50"')
|
||||
res.mustcontain('data-percent="100"')
|
||||
|
||||
|
||||
def test_maximum_password_length_config(testclient):
|
||||
|
@ -333,3 +335,66 @@ def test_maximum_password_length_config(testclient):
|
|||
password_too_long_validator(None, Field("a" * 4096))
|
||||
with pytest.raises(wtforms.ValidationError):
|
||||
password_too_long_validator(None, Field("a" * 4097))
|
||||
|
||||
|
||||
def test_compromised_password_validator(testclient):
|
||||
class Field:
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
compromised_password_validator(None, Field("i'm a little pea"))
|
||||
compromised_password_validator(None, Field("i'm a little chickpea"))
|
||||
compromised_password_validator(None, Field("i'm singing in the rain"))
|
||||
with pytest.raises(wtforms.ValidationError):
|
||||
compromised_password_validator(None, Field("password"))
|
||||
with pytest.raises(wtforms.ValidationError):
|
||||
compromised_password_validator(None, Field("987654321"))
|
||||
with pytest.raises(wtforms.ValidationError):
|
||||
compromised_password_validator(None, Field("correct horse battery staple"))
|
||||
with pytest.raises(wtforms.ValidationError):
|
||||
compromised_password_validator(None, Field("zxcvbn123"))
|
||||
with pytest.raises(wtforms.ValidationError):
|
||||
compromised_password_validator(None, Field("azertyuiop123"))
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_compromised_password_validator_with_failure_of_api_request_and_no_SMTP_in_config(
|
||||
api_get, testclient, logged_user
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
current_app.config["CANAILLE"]["SMTP"] = None
|
||||
|
||||
class Field:
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
compromised_password_validator(None, Field("i'm a little pea"))
|
||||
compromised_password_validator(None, Field("i'm a little chickpea"))
|
||||
compromised_password_validator(None, Field("i'm singing in the rain"))
|
||||
compromised_password_validator(None, Field("password"))
|
||||
compromised_password_validator(None, Field("987654321"))
|
||||
compromised_password_validator(None, Field("correct horse battery staple"))
|
||||
compromised_password_validator(None, Field("zxcvbn123"))
|
||||
compromised_password_validator(None, Field("azertyuiop123"))
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_compromised_password_validator_with_failure_of_api_request_and_only_with_htmx(
|
||||
api_get, testclient, logged_user
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
|
||||
res = testclient.get("/profile/user/settings")
|
||||
res = testclient.post(
|
||||
"/profile/user/settings",
|
||||
{
|
||||
"csrf_token": res.form["csrf_token"].value,
|
||||
"password1": "correct horse battery staple",
|
||||
},
|
||||
headers={
|
||||
"HX-Request": "true",
|
||||
"HX-Trigger-Name": "password1",
|
||||
},
|
||||
)
|
||||
|
||||
res.mustcontain('data-percent="100"')
|
||||
|
|
|
@ -151,6 +151,7 @@ def configuration(smtpd):
|
|||
},
|
||||
"disable_existing_loggers": False,
|
||||
},
|
||||
"ADMIN_EMAIL": "admin_default_mail@mymail.com",
|
||||
},
|
||||
}
|
||||
return conf
|
||||
|
@ -270,6 +271,18 @@ def bar_group(app, admin, backend):
|
|||
backend.delete(group)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admins_group(app, admin, backend):
|
||||
group = models.Group(
|
||||
members=[admin],
|
||||
display_name="admins",
|
||||
)
|
||||
backend.save(group)
|
||||
backend.reload(admin)
|
||||
yield group
|
||||
backend.delete(group)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jpeg_photo():
|
||||
return b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x01,\x01,\x00\x00\xff\xfe\x00\x13Created with GIMP\xff\xe2\x02\xb0ICC_PROFILE\x00\x01\x01\x00\x00\x02\xa0lcms\x040\x00\x00mntrRGB XYZ \x07\xe5\x00\x0c\x00\x08\x00\x0f\x00\x16\x00(acspAPPL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\xd6\x00\x01\x00\x00\x00\x00\xd3-lcms\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\rdesc\x00\x00\x01 \x00\x00\x00@cprt\x00\x00\x01`\x00\x00\x006wtpt\x00\x00\x01\x98\x00\x00\x00\x14chad\x00\x00\x01\xac\x00\x00\x00,rXYZ\x00\x00\x01\xd8\x00\x00\x00\x14bXYZ\x00\x00\x01\xec\x00\x00\x00\x14gXYZ\x00\x00\x02\x00\x00\x00\x00\x14rTRC\x00\x00\x02\x14\x00\x00\x00 gTRC\x00\x00\x02\x14\x00\x00\x00 bTRC\x00\x00\x02\x14\x00\x00\x00 chrm\x00\x00\x024\x00\x00\x00$dmnd\x00\x00\x02X\x00\x00\x00$dmdd\x00\x00\x02|\x00\x00\x00$mluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00$\x00\x00\x00\x1c\x00G\x00I\x00M\x00P\x00 \x00b\x00u\x00i\x00l\x00t\x00-\x00i\x00n\x00 \x00s\x00R\x00G\x00Bmluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00\x1a\x00\x00\x00\x1c\x00P\x00u\x00b\x00l\x00i\x00c\x00 \x00D\x00o\x00m\x00a\x00i\x00n\x00\x00XYZ \x00\x00\x00\x00\x00\x00\xf6\xd6\x00\x01\x00\x00\x00\x00\xd3-sf32\x00\x00\x00\x00\x00\x01\x0cB\x00\x00\x05\xde\xff\xff\xf3%\x00\x00\x07\x93\x00\x00\xfd\x90\xff\xff\xfb\xa1\xff\xff\xfd\xa2\x00\x00\x03\xdc\x00\x00\xc0nXYZ \x00\x00\x00\x00\x00\x00o\xa0\x00\x008\xf5\x00\x00\x03\x90XYZ \x00\x00\x00\x00\x00\x00$\x9f\x00\x00\x0f\x84\x00\x00\xb6\xc4XYZ \x00\x00\x00\x00\x00\x00b\x97\x00\x00\xb7\x87\x00\x00\x18\xd9para\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02ff\x00\x00\xf2\xa7\x00\x00\rY\x00\x00\x13\xd0\x00\x00\n[chrm\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\xa3\xd7\x00\x00T|\x00\x00L\xcd\x00\x00\x99\x9a\x00\x00&g\x00\x00\x0f\\mluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00\x08\x00\x00\x00\x1c\x00G\x00I\x00M\x00Pmluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00\x08\x00\x00\x00\x1c\x00s\x00R\x00G\x00B\xff\xdb\x00C\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xff\xdb\x00C\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xff\xc2\x00\x11\x08\x00\x01\x00\x01\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x10\x03\x10\x00\x00\x01\x7f\x0f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x7f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x03\x01\x01?\x01\x7f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x02\x01\x01?\x01\x7f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x06?\x02\x7f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x01?!\x7f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x03\x01\x01?\x10\x7f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x02\x01\x01?\x10\x7f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x01?\x10\x7f\xff\xd9"
|
||||
|
|
|
@ -36,8 +36,8 @@ def test_invitation(testclient, logged_admin, foo_group, smtpd, backend):
|
|||
assert res.form["emails-0"].value == "someone@domain.tld"
|
||||
assert res.form["groups"].value == [foo_group.id]
|
||||
|
||||
res.form["password1"] = "whatever"
|
||||
res.form["password2"] = "whatever"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little pea"
|
||||
res.form["given_name"] = "George"
|
||||
res.form["family_name"] = "Abitbol"
|
||||
|
||||
|
@ -48,7 +48,7 @@ def test_invitation(testclient, logged_admin, foo_group, smtpd, backend):
|
|||
|
||||
user = backend.get(models.User, user_name="someone")
|
||||
backend.reload(foo_group)
|
||||
assert backend.check_user_password(user, "whatever")[0]
|
||||
assert backend.check_user_password(user, "i'm a little pea")[0]
|
||||
assert user.groups == [foo_group]
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
|
@ -92,8 +92,8 @@ def test_invitation_editable_user_name(
|
|||
assert res.form["groups"].value == [foo_group.id]
|
||||
|
||||
res.form["user_name"] = "djorje"
|
||||
res.form["password1"] = "whatever"
|
||||
res.form["password2"] = "whatever"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little pea"
|
||||
res.form["given_name"] = "George"
|
||||
res.form["family_name"] = "Abitbol"
|
||||
|
||||
|
@ -104,7 +104,7 @@ def test_invitation_editable_user_name(
|
|||
|
||||
user = backend.get(models.User, user_name="djorje")
|
||||
backend.reload(foo_group)
|
||||
assert backend.check_user_password(user, "whatever")[0]
|
||||
assert backend.check_user_password(user, "i'm a little pea")[0]
|
||||
assert user.groups == [foo_group]
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
|
@ -141,8 +141,8 @@ def test_generate_link(testclient, logged_admin, foo_group, smtpd, backend):
|
|||
assert res.form["emails-0"].value == "sometwo@domain.tld"
|
||||
assert res.form["groups"].value == [foo_group.id]
|
||||
|
||||
res.form["password1"] = "whatever"
|
||||
res.form["password2"] = "whatever"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little pea"
|
||||
res.form["given_name"] = "George"
|
||||
res.form["family_name"] = "Abitbol"
|
||||
|
||||
|
@ -151,7 +151,7 @@ def test_generate_link(testclient, logged_admin, foo_group, smtpd, backend):
|
|||
|
||||
user = backend.get(models.User, user_name="sometwo")
|
||||
backend.reload(foo_group)
|
||||
assert backend.check_user_password(user, "whatever")[0]
|
||||
assert backend.check_user_password(user, "i'm a little pea")[0]
|
||||
assert user.groups == [foo_group]
|
||||
|
||||
with testclient.session_transaction() as sess:
|
||||
|
@ -323,8 +323,8 @@ def test_groups_are_saved_even_when_user_does_not_have_read_permission(
|
|||
assert res.form["groups"].value == [foo_group.id]
|
||||
assert "readonly" in res.form["groups"].attrs
|
||||
|
||||
res.form["password1"] = "whatever"
|
||||
res.form["password2"] = "whatever"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little pea"
|
||||
res.form["given_name"] = "George"
|
||||
res.form["family_name"] = "Abitbol"
|
||||
|
||||
|
|
|
@ -156,6 +156,118 @@ def test_profile_settings_too_long_password(testclient, logged_user):
|
|||
)
|
||||
|
||||
|
||||
def test_profile_settings_compromised_password(testclient, logged_user):
|
||||
"""Tests if password is compromised."""
|
||||
|
||||
def with_different_values(password, message):
|
||||
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("aaaaaaaa", "This password is compromised.")
|
||||
with_different_values("azertyuiop", "This password is compromised.")
|
||||
with_different_values("a" * 1000, 'data-percent="25"')
|
||||
with_different_values("i'm a little pea", 'data-percent="100"')
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_profile_settings_compromised_password_request_api_failed_but_password_updated(
|
||||
api_get, testclient, logged_user, backend
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
|
||||
current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"] = {"groups": "admins"}
|
||||
|
||||
res = testclient.get("/profile/user/settings", status=200)
|
||||
|
||||
res.form["password1"] = "123456789"
|
||||
res.form["password2"] = "123456789"
|
||||
|
||||
res = res.form.submit(name="action", value="edit-settings")
|
||||
|
||||
assert (
|
||||
"error",
|
||||
"Password compromise investigation failed. Please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert ("success", "Profile updated successfully.") in res.flashes
|
||||
|
||||
backend.reload(logged_user)
|
||||
|
||||
assert logged_user.user_name == "user"
|
||||
assert backend.check_user_password(logged_user, "123456789")[0]
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_compromised_password_validator_with_failure_of_api_request_and_success_mail_to_admins_from_settings_form(
|
||||
api_get, testclient, backend, admins_group, user, logged_user
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
|
||||
def with_and_without_admin_group(group):
|
||||
current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"] = group
|
||||
|
||||
res = testclient.get("/profile/user/settings", status=200)
|
||||
|
||||
res.form.user = user
|
||||
res.form["password1"] = "123456789"
|
||||
res.form["password2"] = "123456789"
|
||||
|
||||
res = res.form.submit(name="action", value="edit-settings")
|
||||
|
||||
assert (
|
||||
"error",
|
||||
"Password compromise investigation failed. Please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert (
|
||||
"success",
|
||||
"We have informed your administrator about the failure of the password compromise investigation.",
|
||||
) in res.flashes
|
||||
assert ("success", "Profile updated successfully.") in res.flashes
|
||||
|
||||
with_and_without_admin_group({"groups": "admins"})
|
||||
with_and_without_admin_group(None)
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_compromised_password_validator_with_failure_of_api_request_and_fail_to_send_mail_to_admins_from_settings_form(
|
||||
api_get, testclient, backend, admins_group, user, logged_user
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"] = {"groups": "admins"}
|
||||
current_app.config["CANAILLE"]["SMTP"]["TLS"] = False
|
||||
|
||||
assert not backend.query(models.User, user_name="newuser")
|
||||
|
||||
res = testclient.get("/profile/user/settings", status=200)
|
||||
res.form.user = user
|
||||
res.form["password1"] = "123456789"
|
||||
res.form["password2"] = "123456789"
|
||||
|
||||
res = res.form.submit(name="action", value="edit-settings")
|
||||
|
||||
assert (
|
||||
"error",
|
||||
"Password compromise investigation failed. Please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert (
|
||||
"error",
|
||||
"An error occurred while communicating the incident to the administrators. "
|
||||
"Please update your password as soon as possible. "
|
||||
"If this still happens, please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert ("success", "Profile updated successfully.") in res.flashes
|
||||
|
||||
|
||||
def test_edition_without_groups(
|
||||
testclient,
|
||||
logged_user,
|
||||
|
@ -181,18 +293,18 @@ def test_edition_without_groups(
|
|||
def test_password_change(testclient, logged_user, backend, caplog):
|
||||
res = testclient.get("/profile/user/settings", status=200)
|
||||
|
||||
res.form["password1"] = "new_password"
|
||||
res.form["password2"] = "new_password"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little pea"
|
||||
|
||||
res = res.form.submit(name="action", value="edit-settings").follow()
|
||||
|
||||
backend.reload(logged_user)
|
||||
assert backend.check_user_password(logged_user, "new_password")[0]
|
||||
assert backend.check_user_password(logged_user, "i'm a little pea")[0]
|
||||
|
||||
res = testclient.get("/profile/user/settings", status=200)
|
||||
|
||||
res.form["password1"] = "correct horse battery staple"
|
||||
res.form["password2"] = "correct horse battery staple"
|
||||
res.form["password1"] = "i'm a little chickpea"
|
||||
res.form["password2"] = "i'm a little chickpea"
|
||||
|
||||
res = res.form.submit(name="action", value="edit-settings")
|
||||
assert ("success", "Profile updated successfully.") in res.flashes
|
||||
|
@ -206,14 +318,14 @@ def test_password_change(testclient, logged_user, backend, caplog):
|
|||
res = res.follow()
|
||||
|
||||
backend.reload(logged_user)
|
||||
assert backend.check_user_password(logged_user, "correct horse battery staple")[0]
|
||||
assert backend.check_user_password(logged_user, "i'm a little chickpea")[0]
|
||||
|
||||
|
||||
def test_password_change_fail(testclient, logged_user, backend):
|
||||
res = testclient.get("/profile/user/settings", status=200)
|
||||
|
||||
res.form["password1"] = "new_password"
|
||||
res.form["password2"] = "other_password"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little chickpea"
|
||||
|
||||
res = res.form.submit(name="action", value="edit-settings", status=200)
|
||||
|
||||
|
@ -222,7 +334,7 @@ def test_password_change_fail(testclient, logged_user, backend):
|
|||
|
||||
res = testclient.get("/profile/user/settings", status=200)
|
||||
|
||||
res.form["password1"] = "new_password"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = ""
|
||||
|
||||
res = res.form.submit(name="action", value="edit-settings", status=200)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from unittest import mock
|
||||
|
||||
import time_machine
|
||||
from flask import current_app
|
||||
from flask import url_for
|
||||
|
||||
from canaille.app import models
|
||||
|
@ -15,8 +16,8 @@ def test_registration_without_email_validation(testclient, backend, foo_group):
|
|||
assert not backend.query(models.User, user_name="newuser")
|
||||
res = testclient.get(url_for("core.account.registration"), status=200)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "password"
|
||||
res.form["password2"] = "password"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little pea"
|
||||
res.form["family_name"] = "newuser"
|
||||
res.form["emails-0"] = "newuser@example.com"
|
||||
res = res.form.submit()
|
||||
|
@ -64,8 +65,8 @@ def test_registration_with_email_validation(testclient, backend, smtpd, foo_grou
|
|||
with time_machine.travel("2020-01-01 02:01:00+00:00", tick=False):
|
||||
res = testclient.get(registration_url, status=200)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "password"
|
||||
res.form["password2"] = "password"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little pea"
|
||||
res.form["family_name"] = "newuser"
|
||||
res = res.form.submit()
|
||||
|
||||
|
@ -150,3 +151,161 @@ def test_registration_mail_error(SMTP, testclient, backend, smtpd, foo_group):
|
|||
)
|
||||
]
|
||||
assert len(smtpd.messages) == 0
|
||||
|
||||
|
||||
def test_registration_with_compromised_password(testclient, backend, foo_group):
|
||||
"""Tests a nominal registration with compromised password."""
|
||||
testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
|
||||
testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False
|
||||
|
||||
assert not backend.query(models.User, user_name="newuser")
|
||||
res = testclient.get(url_for("core.account.registration"), status=200)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "123456789"
|
||||
res.form["password2"] = "123456789"
|
||||
res.form["family_name"] = "newuser"
|
||||
res.form["emails-0"] = "newuser@example.com"
|
||||
res = res.form.submit()
|
||||
res.mustcontain("This password is compromised.")
|
||||
|
||||
user = backend.get(models.User, user_name="newuser")
|
||||
assert user is None
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_registration_with_compromised_password_request_api_failed_but_account_created(
|
||||
api_get, testclient, backend
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"] = {"groups": "admins"}
|
||||
testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
|
||||
testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False
|
||||
|
||||
assert not backend.query(models.User, user_name="newuser")
|
||||
|
||||
res = testclient.get(url_for("core.account.registration"), status=200)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "123456789"
|
||||
res.form["password2"] = "123456789"
|
||||
res.form["family_name"] = "newuser"
|
||||
res.form["emails-0"] = "newuser@example.com"
|
||||
|
||||
res = res.form.submit()
|
||||
|
||||
assert (
|
||||
"error",
|
||||
"Password compromise investigation failed. Please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert ("success", "Your account has been created successfully.") in res.flashes
|
||||
|
||||
user = backend.get(models.User, user_name="newuser")
|
||||
assert user
|
||||
backend.delete(user)
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_compromised_password_validator_with_failure_of_api_request_and_success_mail_to_admins_from_register_form_with_admin_group(
|
||||
api_get, testclient, backend, admins_group
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"] = {"groups": "admins"}
|
||||
testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
|
||||
testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False
|
||||
|
||||
assert not backend.query(models.User, user_name="newuser")
|
||||
|
||||
res = testclient.get(url_for("core.account.registration"), status=200)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "123456789"
|
||||
res.form["password2"] = "123456789"
|
||||
res.form["family_name"] = "newuser"
|
||||
res.form["emails-0"] = "newuser@example.com"
|
||||
|
||||
res = res.form.submit()
|
||||
|
||||
assert (
|
||||
"error",
|
||||
"Password compromise investigation failed. Please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert (
|
||||
"success",
|
||||
"We have informed your administrator about the failure of the password compromise investigation.",
|
||||
) in res.flashes
|
||||
assert ("success", "Your account has been created successfully.") in res.flashes
|
||||
|
||||
user = backend.get(models.User, user_name="newuser")
|
||||
assert user
|
||||
backend.delete(user)
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_compromised_password_validator_with_failure_of_api_request_and_success_mail_to_admins_from_register_form_without_admin_group(
|
||||
api_get, testclient, backend, admins_group
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"] = None
|
||||
testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
|
||||
testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False
|
||||
|
||||
assert not backend.query(models.User, user_name="newuser")
|
||||
|
||||
res = testclient.get(url_for("core.account.registration"), status=200)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "123456789"
|
||||
res.form["password2"] = "123456789"
|
||||
res.form["family_name"] = "newuser"
|
||||
res.form["emails-0"] = "newuser@example.com"
|
||||
|
||||
res = res.form.submit()
|
||||
|
||||
assert (
|
||||
"error",
|
||||
"Password compromise investigation failed. Please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert (
|
||||
"success",
|
||||
"We have informed your administrator about the failure of the password compromise investigation.",
|
||||
) in res.flashes
|
||||
assert ("success", "Your account has been created successfully.") in res.flashes
|
||||
|
||||
user = backend.get(models.User, user_name="newuser")
|
||||
assert user
|
||||
backend.delete(user)
|
||||
|
||||
|
||||
@mock.patch("requests.api.get")
|
||||
def test_compromised_password_validator_with_failure_of_api_request_and_fail_to_send_mail_to_admins_from_register_form(
|
||||
api_get, testclient, backend, admins_group
|
||||
):
|
||||
api_get.side_effect = mock.Mock(side_effect=Exception())
|
||||
current_app.config["CANAILLE"]["ACL"]["ADMIN"]["FILTER"] = {"groups": "admins"}
|
||||
current_app.config["CANAILLE"]["SMTP"]["TLS"] = False
|
||||
testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
|
||||
testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False
|
||||
|
||||
assert not backend.query(models.User, user_name="newuser")
|
||||
|
||||
res = testclient.get(url_for("core.account.registration"), status=200)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "123456789"
|
||||
res.form["password2"] = "123456789"
|
||||
res.form["family_name"] = "newuser"
|
||||
res.form["emails-0"] = "newuser@example.com"
|
||||
|
||||
res = res.form.submit()
|
||||
|
||||
assert (
|
||||
"error",
|
||||
"Password compromise investigation failed. Please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert (
|
||||
"error",
|
||||
"An error occurred while communicating the incident to the administrators. "
|
||||
"Please update your password as soon as possible. "
|
||||
"If this still happens, please contact the administrators.",
|
||||
) in res.flashes
|
||||
assert ("success", "Your account has been created successfully.") in res.flashes
|
||||
|
||||
user = backend.get(models.User, user_name="newuser")
|
||||
assert user
|
||||
backend.delete(user)
|
||||
|
|
|
@ -205,8 +205,8 @@ def test_prompt_create_not_logged(testclient, trusted_client, smtpd):
|
|||
# Fill the user creation form
|
||||
res = testclient.get(registration_url)
|
||||
res.form["user_name"] = "newuser"
|
||||
res.form["password1"] = "password"
|
||||
res.form["password2"] = "password"
|
||||
res.form["password1"] = "i'm a little pea"
|
||||
res.form["password2"] = "i'm a little pea"
|
||||
res.form["family_name"] = "newuser"
|
||||
res = res.form.submit()
|
||||
|
||||
|
|
2
uv.lock
2
uv.lock
|
@ -127,6 +127,7 @@ dependencies = [
|
|||
{ name = "flask" },
|
||||
{ name = "flask-wtf" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "requests" },
|
||||
{ name = "wtforms" },
|
||||
]
|
||||
|
||||
|
@ -220,6 +221,7 @@ requires-dist = [
|
|||
{ name = "pydantic-settings", specifier = ">=2.0.3" },
|
||||
{ name = "python-ldap", marker = "extra == 'ldap'", specifier = ">=3.4.0" },
|
||||
{ name = "pytz", marker = "extra == 'front'", specifier = ">=2022.7" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.0.0" },
|
||||
{ name = "sqlalchemy", marker = "extra == 'sqlite'", specifier = ">=2.0.23" },
|
||||
{ name = "sqlalchemy", extras = ["mysql"], marker = "extra == 'mysql'", specifier = ">=2.0.23" },
|
||||
|
|
Loading…
Reference in a new issue