invitations expire after 48h

This commit is contained in:
Camille 2022-01-01 10:56:48 +00:00 committed by Éloi Rivard
parent ebc16b91c2
commit db1d011a3b
10 changed files with 286 additions and 120 deletions

View file

@ -1,4 +1,9 @@
import io import io
from dataclasses import astuple
from dataclasses import dataclass
from datetime import datetime
from datetime import timedelta
from typing import List
import pkg_resources import pkg_resources
import wtforms import wtforms
@ -162,6 +167,32 @@ def users(user):
return render_template("users.html", users=users, menuitem="users") return render_template("users.html", users=users, menuitem="users")
@dataclass
class Invitation:
creation_date_isoformat: str
uid: str
mail: str
groups: List[str]
@property
def creation_date(self):
return datetime.fromisoformat(self.creation_date_isoformat)
def has_expired(self):
DEFAULT_INVITATION_DURATION = 2 * 24 * 60 * 60
return datetime.now() - self.creation_date > timedelta(
seconds=current_app.config.get(
"INVITATION_EXPIRATION", DEFAULT_INVITATION_DURATION
)
)
def b64(self):
return obj_to_b64(astuple(self))
def profile_hash(self):
return profile_hash(*astuple(self))
@bp.route("/invite", methods=["GET", "POST"]) @bp.route("/invite", methods=["GET", "POST"])
@smtp_needed() @smtp_needed()
@permissions_needed("manage_users") @permissions_needed("manage_users")
@ -173,10 +204,16 @@ def user_invitation(user):
form_validated = False form_validated = False
if request.form and form.validate(): if request.form and form.validate():
form_validated = True form_validated = True
invitation = Invitation(
datetime.now().isoformat(),
form.uid.data,
form.mail.data,
form.groups.data,
)
registration_url = url_for( registration_url = url_for(
"account.registration", "account.registration",
data=obj_to_b64([form.uid.data, form.mail.data, form.groups.data]), data=invitation.b64(),
hash=profile_hash(form.uid.data, form.mail.data, form.groups.data), hash=invitation.profile_hash(),
_external=True, _external=True,
) )
@ -223,7 +260,7 @@ def profile_creation(user):
@bp.route("/register/<data>/<hash>", methods=["GET", "POST"]) @bp.route("/register/<data>/<hash>", methods=["GET", "POST"])
def registration(data, hash): def registration(data, hash):
try: try:
data = b64_to_obj(data) invitation = Invitation(*b64_to_obj(data))
except: except:
flash( flash(
_("The invitation link that brought you here was invalid."), _("The invitation link that brought you here was invalid."),
@ -231,7 +268,14 @@ def registration(data, hash):
) )
return redirect(url_for("account.index")) return redirect(url_for("account.index"))
if User.get(data[0]): if invitation.has_expired():
flash(
_("The invitation link that brought you here has expired."),
"error",
)
return redirect(url_for("account.index"))
if User.get(invitation.uid):
flash( flash(
_("Your account has already been created."), _("Your account has already been created."),
"error", "error",
@ -245,18 +289,14 @@ def registration(data, hash):
) )
return redirect(url_for("account.index")) return redirect(url_for("account.index"))
if hash != profile_hash(*data): if hash != invitation.profile_hash():
flash( flash(
_("The invitation link that brought you here was invalid."), _("The invitation link that brought you here was invalid."),
"error", "error",
) )
return redirect(url_for("account.index")) return redirect(url_for("account.index"))
data = { data = {"uid": invitation.uid, "mail": invitation.mail, "groups": invitation.groups}
"uid": data[0],
"mail": data[1],
"groups": data[2],
}
readable_fields, writable_fields = default_fields() readable_fields, writable_fields = default_fields()

View file

@ -40,6 +40,10 @@ OIDC_METADATA_FILE = "canaille/conf/openid-configuration.json"
# wether the login exists or not. # wether the login exists or not.
# HIDE_INVALID_LOGINS = false # HIDE_INVALID_LOGINS = false
# The validity duration of registration invitations, in seconds.
# Defaults to 2 days
# INVITATION_EXPIRATION = 172800
[LOGGING] [LOGGING]
# LEVEL can be one value among: # LEVEL can be one value among:
# DEBUG, INFO, WARNING, ERROR, CRITICAL # DEBUG, INFO, WARNING, ERROR, CRITICAL

View file

@ -5,13 +5,13 @@
<div class="ui segment"> <div class="ui segment">
<h2 class="ui center aligned header"> <h2 class="ui center aligned header">
<div class="content"> <div class="content">
{{ _("Test d'envoi") }} {{ _("Mail sending test") }}
</div> </div>
</h2> </h2>
<div class="ui info message"> <div class="ui info message">
{% trans -%} {% trans -%}
This form will a fake invitation email to the address you want. This form will send a fake invitation email to the address you want.
This should be used for testing mail configuration. This should be used for testing mail configuration.
{%- endtrans %} {%- endtrans %}
</div> </div>

View file

@ -3,14 +3,14 @@
# This file is distributed under the same license as the PROJECT project. # This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2020. # FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
# Éloi Rivard <eloi.rivard@aquilenet.fr>, 2020-2021. # Éloi Rivard <eloi.rivard@aquilenet.fr>, 2020-2021.
# Camille <camille@yaal.coop>, 2021. # Camille <camille@yaal.coop>, 2021-2022.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: contact@yaal.fr\n" "Report-Msgid-Bugs-To: contact@yaal.fr\n"
"POT-Creation-Date: 2021-12-23 18:45+0100\n" "POT-Creation-Date: 2022-01-01 11:24+0100\n"
"PO-Revision-Date: 2021-12-23 18:48+0100\n" "PO-Revision-Date: 2022-01-01 11:26+0100\n"
"Last-Translator: Camille <camille@yaal.coop>\n" "Last-Translator: Camille <camille@yaal.coop>\n"
"Language: fr\n" "Language: fr\n"
"Language-Team: French <contact@yaal.coop>\n" "Language-Team: French <contact@yaal.coop>\n"
@ -21,34 +21,34 @@ msgstr ""
"Generated-By: Babel 2.9.1\n" "Generated-By: Babel 2.9.1\n"
"X-Generator: Gtranslator 40.0\n" "X-Generator: Gtranslator 40.0\n"
#: canaille/__init__.py:134 #: canaille/__init__.py:131
msgid "Could not connect to the LDAP server '{uri}'" msgid "Could not connect to the LDAP server '{uri}'"
msgstr "Impossible de se connecter au serveur LDAP '{uri}'" msgstr "Impossible de se connecter au serveur LDAP '{uri}'"
#: canaille/__init__.py:150 #: canaille/__init__.py:147
msgid "LDAP authentication failed with user '{user}'" msgid "LDAP authentication failed with user '{user}'"
msgstr "" msgstr ""
"L'authentification au serveur LDAP a échoué pour l'utilisateur '{user}'" "L'authentification au serveur LDAP a échoué pour l'utilisateur '{user}'"
#: canaille/account.py:77 canaille/account.py:102 canaille/oauth.py:77 #: canaille/account.py:82 canaille/account.py:107 canaille/oauth.py:77
msgid "Login failed, please check your information" msgid "Login failed, please check your information"
msgstr "La connexion a échoué, veuillez vérifier vos informations." msgstr "La connexion a échoué, veuillez vérifier vos informations."
#: canaille/account.py:108 #: canaille/account.py:113
#, python-format #, python-format
msgid "Connection successful. Welcome %(user)s" msgid "Connection successful. Welcome %(user)s"
msgstr "Connexion réussie. Bienvenue %(user)s" msgstr "Connexion réussie. Bienvenue %(user)s"
#: canaille/account.py:121 #: canaille/account.py:126
#, python-format #, python-format
msgid "You have been disconnected. See you next time %(user)s" msgid "You have been disconnected. See you next time %(user)s"
msgstr "Vous avez été déconnecté·e. À bientôt %(user)s" msgstr "Vous avez été déconnecté·e. À bientôt %(user)s"
#: canaille/account.py:138 #: canaille/account.py:143
msgid "Could not send the password initialization link." msgid "Could not send the password initialization link."
msgstr "Impossible d'envoyer le courriel d'initialisation de mot de passe." msgstr "Impossible d'envoyer le courriel d'initialisation de mot de passe."
#: canaille/account.py:143 #: canaille/account.py:148
msgid "" msgid ""
"A password initialization link has been sent at your email address. You " "A password initialization link has been sent at your email address. You "
"should receive it within 10 minutes." "should receive it within 10 minutes."
@ -56,35 +56,39 @@ msgstr ""
"Un lien d'initialisation de votre mot de passe vous a été envoyé par mail. " "Un lien d'initialisation de votre mot de passe vous a été envoyé par mail. "
"Vous devriez le recevoir d'ici une dizaine de minutes." "Vous devriez le recevoir d'ici une dizaine de minutes."
#: canaille/account.py:149 canaille/account.py:353 #: canaille/account.py:154 canaille/account.py:388
msgid "Could not send the password initialization email" msgid "Could not send the password initialization email"
msgstr "Impossible d'envoyer le courriel d'initialisation de mot de passe." msgstr "Impossible d'envoyer le courriel d'initialisation de mot de passe."
#: canaille/account.py:208 canaille/account.py:279 #: canaille/account.py:240 canaille/account.py:314
msgid "User account creation failed." msgid "User account creation failed."
msgstr "La création du compte utilisateur a échoué." msgstr "La création du compte utilisateur a échoué."
#: canaille/account.py:229 canaille/account.py:250 #: canaille/account.py:261 canaille/account.py:289
msgid "The invitation link that brought you here was invalid." msgid "The invitation link that brought you here was invalid."
msgstr "Le lien d'invitation qui vous a amené ici est invalide." msgstr "Le lien d'invitation qui vous a amené ici est invalide."
#: canaille/account.py:236 #: canaille/account.py:268
msgid "The invitation link that brought you here has expired."
msgstr "Le lien d'invitation qui vous a amené ici a expiré."
#: canaille/account.py:275
msgid "Your account has already been created." msgid "Your account has already been created."
msgstr "Votre compte a déjà été créé." msgstr "Votre compte a déjà été créé."
#: canaille/account.py:243 #: canaille/account.py:282
msgid "You are already logged in, you cannot create an account." msgid "You are already logged in, you cannot create an account."
msgstr "Vous êtes déjà connectés, vous ne pouvez pas créer de compte." msgstr "Vous êtes déjà connectés, vous ne pouvez pas créer de compte."
#: canaille/account.py:284 #: canaille/account.py:319
msgid "You account has been created successfuly." msgid "You account has been created successfuly."
msgstr "Votre compte utilisateur a été créé avec succès." msgstr "Votre compte utilisateur a été créé avec succès."
#: canaille/account.py:326 #: canaille/account.py:361
msgid "User account creation succeed." msgid "User account creation succeed."
msgstr "La création du compte utilisateur a réussi." msgstr "La création du compte utilisateur a réussi."
#: canaille/account.py:347 #: canaille/account.py:382
msgid "" msgid ""
"A password initialization link has been sent at the user email address. It " "A password initialization link has been sent at the user email address. It "
"should be received within 10 minutes." "should be received within 10 minutes."
@ -92,7 +96,7 @@ msgstr ""
"Un lien d'initialisation de mot de passe a été envoyé à l'utilisateur par " "Un lien d'initialisation de mot de passe a été envoyé à l'utilisateur par "
"mail. Il devrait arriver d'ici une dizaine de minutes." "mail. Il devrait arriver d'ici une dizaine de minutes."
#: canaille/account.py:361 #: canaille/account.py:396
msgid "" msgid ""
"A password reset link has been sent at the user email address. It should be " "A password reset link has been sent at the user email address. It should be "
"received within 10 minutes." "received within 10 minutes."
@ -100,28 +104,28 @@ msgstr ""
"Un lien de réinitialisation de mot de passe a été envoyé à l'utilisateur par " "Un lien de réinitialisation de mot de passe a été envoyé à l'utilisateur par "
"mail. Il devrait arriver d'ici une dizaine de minutes." "mail. Il devrait arriver d'ici une dizaine de minutes."
#: canaille/account.py:367 #: canaille/account.py:402
msgid "Could not send the password reset email" msgid "Could not send the password reset email"
msgstr "Impossible d'envoyer le lien de réinitialisation." msgstr "Impossible d'envoyer le lien de réinitialisation."
#: canaille/account.py:398 #: canaille/account.py:433
msgid "Profile edition failed." msgid "Profile edition failed."
msgstr "L'édition du profil a échoué." msgstr "L'édition du profil a échoué."
#: canaille/account.py:426 #: canaille/account.py:461
msgid "Profile updated successfuly." msgid "Profile updated successfuly."
msgstr "Le profil a été mis à jour avec succès." msgstr "Le profil a été mis à jour avec succès."
#: canaille/account.py:446 #: canaille/account.py:481
#, python-format #, python-format
msgid "The user %(user)s has been sucessfuly deleted" msgid "The user %(user)s has been sucessfuly deleted"
msgstr "L'utilisateur %(user)s a bien été supprimé" msgstr "L'utilisateur %(user)s a bien été supprimé"
#: canaille/account.py:470 #: canaille/account.py:505
msgid "Could not send the password reset link." msgid "Could not send the password reset link."
msgstr "Impossible d'envoyer le lien de réinitialisation." msgstr "Impossible d'envoyer le lien de réinitialisation."
#: canaille/account.py:477 canaille/account.py:488 #: canaille/account.py:512 canaille/account.py:523
msgid "" msgid ""
"A password reset link has been sent at your email address. You should " "A password reset link has been sent at your email address. You should "
"receive it within 10 minutes." "receive it within 10 minutes."
@ -129,15 +133,15 @@ msgstr ""
"Un lien de ré-initialisation de votre mot de passe vous a été envoyé par " "Un lien de ré-initialisation de votre mot de passe vous a été envoyé par "
"mail. Vous devriez le recevoir d'ici une dizaine de minutes." "mail. Vous devriez le recevoir d'ici une dizaine de minutes."
#: canaille/account.py:494 #: canaille/account.py:529
msgid "Could not reset your password" msgid "Could not reset your password"
msgstr "Impossible de réinitialiser votre mot de passe" msgstr "Impossible de réinitialiser votre mot de passe"
#: canaille/account.py:508 #: canaille/account.py:543
msgid "The password reset link that brought you here was invalid." msgid "The password reset link that brought you here was invalid."
msgstr "Le lien de réinitialisation qui vous a amené ici est invalide." msgstr "Le lien de réinitialisation qui vous a amené ici est invalide."
#: canaille/account.py:517 #: canaille/account.py:552
msgid "Your password has been updated successfuly" msgid "Your password has been updated successfuly"
msgstr "Votre mot de passe a correctement été mis à jour." msgstr "Votre mot de passe a correctement été mis à jour."
@ -189,8 +193,8 @@ msgstr "L'identifiant '{login}' n'existe pas"
msgid "Login" msgid "Login"
msgstr "Identifiant" msgstr "Identifiant"
#: canaille/forms.py:47 canaille/forms.py:71 canaille/forms.py:122 #: canaille/admin/mail.py:29 canaille/forms.py:47 canaille/forms.py:71
#: canaille/forms.py:222 #: canaille/forms.py:122 canaille/forms.py:222
msgid "jane@doe.com" msgid "jane@doe.com"
msgstr "camille@dupont.fr" msgstr "camille@dupont.fr"
@ -311,11 +315,11 @@ msgstr "L'édition du groupe a échoué."
msgid "The group %(group)s has been sucessfully deleted" msgid "The group %(group)s has been sucessfully deleted"
msgstr "Le groupe %(group)s a bien été supprimé" msgstr "Le groupe %(group)s a bien été supprimé"
#: canaille/admin/mail.py:79 canaille/mails.py:25 #: canaille/admin/mail.py:108 canaille/mails.py:25
msgid "Password reset on {website_name}" msgid "Password reset on {website_name}"
msgstr "Réinitialisation du mot de passe sur {website_name}" msgstr "Réinitialisation du mot de passe sur {website_name}"
#: canaille/admin/mail.py:37 canaille/mails.py:65 #: canaille/admin/mail.py:66 canaille/mails.py:65
msgid "Password initialization on {website_name}" msgid "Password initialization on {website_name}"
msgstr "Initialisation de votre mot de passe sur {website_name}" msgstr "Initialisation de votre mot de passe sur {website_name}"
@ -431,11 +435,19 @@ msgstr "Le client a été édité."
msgid "The client has been deleted." msgid "The client has been deleted."
msgstr "Le client a été supprimé." msgstr "Le client a été supprimé."
#: canaille/admin/mail.py:121 #: canaille/admin/mail.py:23 canaille/templates/userlist.html:14
msgid "Email"
msgstr "Courriel"
#: canaille/admin/mail.py:42 canaille/admin/mail.py:44
msgid "The test invitation mail has been sent correctly"
msgstr "L'invitation de test a été envoyée correctement"
#: canaille/admin/mail.py:150
msgid "Invitation on {website_name}" msgid "Invitation on {website_name}"
msgstr "Invitation sur {website_name}" msgstr "Invitation sur {website_name}"
#: canaille/templates/about.html:12 canaille/themes/default/base.html:98 #: canaille/templates/about.html:12 canaille/themes/default/base.html:102
msgid "About canaille" msgid "About canaille"
msgstr "À propos de canaille" msgstr "À propos de canaille"
@ -583,11 +595,11 @@ msgstr "Envoyer le courriel d'initialisation"
#: canaille/templates/admin/client_edit.html:35 #: canaille/templates/admin/client_edit.html:35
#: canaille/templates/admin/client_edit.html:44 #: canaille/templates/admin/client_edit.html:44
#: canaille/templates/admin/client_edit.html:53 #: canaille/templates/admin/client_edit.html:53
#: canaille/templates/fomanticui.html:62 #: canaille/templates/fomanticui.html:64
msgid "This field is not editable" msgid "This field is not editable"
msgstr "Ce champ n'est pas modifiable" msgstr "Ce champ n'est pas modifiable"
#: canaille/templates/fomanticui.html:66 #: canaille/templates/fomanticui.html:68
msgid "This field is required" msgid "This field is required"
msgstr "Ce champ est requis" msgstr "Ce champ est requis"
@ -618,6 +630,7 @@ msgstr ""
msgid "Send again" msgid "Send again"
msgstr "Envoyer à nouveau" msgstr "Envoyer à nouveau"
#: canaille/templates/admin/mails.html:23
#: canaille/templates/forgotten-password.html:40 #: canaille/templates/forgotten-password.html:40
msgid "Send" msgid "Send"
msgstr "Envoyer" msgstr "Envoyer"
@ -934,7 +947,7 @@ msgstr ""
msgid "Send mail" msgid "Send mail"
msgstr "Envoyer l'email" msgstr "Envoyer l'email"
#: canaille/templates/admin/mails.html:34 canaille/templates/profile.html:221 #: canaille/templates/admin/mails.html:56 canaille/templates/profile.html:221
#: canaille/templates/reset-password.html:11 #: canaille/templates/reset-password.html:11
#: canaille/templates/reset-password.html:16 #: canaille/templates/reset-password.html:16
msgid "Password reset" msgid "Password reset"
@ -964,10 +977,6 @@ msgstr "Prendre l'identité"
msgid "Invite a user" msgid "Invite a user"
msgstr "Inviter un utilisateur" msgstr "Inviter un utilisateur"
#: canaille/templates/userlist.html:14
msgid "Email"
msgstr "Courriel"
#: canaille/templates/users.html:18 #: canaille/templates/users.html:18
msgid "Add a user" msgid "Add a user"
msgstr "Nouvel utilisateur" msgstr "Nouvel utilisateur"
@ -1049,17 +1058,29 @@ msgstr "Ajouter un client"
msgid "URL" msgid "URL"
msgstr "URL" msgstr "URL"
#: canaille/templates/admin/mails.html:7 #: canaille/templates/admin/mails.html:8
msgid "Mail sending test"
msgstr "Test d'envoi d'email"
#: canaille/templates/admin/mails.html:13
msgid ""
"This form will send a fake invitation email to the address you want.\n"
" This should be used for testing mail configuration."
msgstr ""
"Ce formulaire enverra un faux mail d'invitation à l'adresse que vous "
"indiquerez. À utiliser pour tester les configurations de mail."
#: canaille/templates/admin/mails.html:32
msgid "Email preview" msgid "Email preview"
msgstr "Prévisualisation des emails" msgstr "Prévisualisation des emails"
#: canaille/templates/admin/mails.html:22 #: canaille/templates/admin/mails.html:44
#: canaille/templates/mail/firstlogin.html:19 #: canaille/templates/mail/firstlogin.html:19
#: canaille/templates/mail/reset.txt:1 #: canaille/templates/mail/reset.txt:1
msgid "Password initialization" msgid "Password initialization"
msgstr "Initialisation du mot de passe" msgstr "Initialisation du mot de passe"
#: canaille/templates/admin/mails.html:46 #: canaille/templates/admin/mails.html:68
msgid "Invitation" msgid "Invitation"
msgstr "Invitation" msgstr "Invitation"
@ -1189,7 +1210,11 @@ msgstr "Jetons"
msgid "Codes" msgid "Codes"
msgstr "Codes" msgstr "Codes"
#: canaille/themes/default/base.html:84 #: canaille/themes/default/base.html:81
msgid "Emails"
msgstr "Courriels"
#: canaille/themes/default/base.html:88
msgid "Log out" msgid "Log out"
msgstr "Déconnexion" msgstr "Déconnexion"

View file

@ -1,14 +1,14 @@
# Translations template for PROJECT. # Translations template for PROJECT.
# Copyright (C) 2021 ORGANIZATION # Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project. # This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021. # FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-12-23 18:45+0100\n" "POT-Creation-Date: 2022-01-01 11:24+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,114 +17,118 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n" "Generated-By: Babel 2.9.1\n"
#: canaille/__init__.py:134 #: canaille/__init__.py:131
msgid "Could not connect to the LDAP server '{uri}'" msgid "Could not connect to the LDAP server '{uri}'"
msgstr "" msgstr ""
#: canaille/__init__.py:150 #: canaille/__init__.py:147
msgid "LDAP authentication failed with user '{user}'" msgid "LDAP authentication failed with user '{user}'"
msgstr "" msgstr ""
#: canaille/account.py:77 canaille/account.py:102 canaille/oauth.py:77 #: canaille/account.py:82 canaille/account.py:107 canaille/oauth.py:77
msgid "Login failed, please check your information" msgid "Login failed, please check your information"
msgstr "" msgstr ""
#: canaille/account.py:108 #: canaille/account.py:113
#, python-format #, python-format
msgid "Connection successful. Welcome %(user)s" msgid "Connection successful. Welcome %(user)s"
msgstr "" msgstr ""
#: canaille/account.py:121 #: canaille/account.py:126
#, python-format #, python-format
msgid "You have been disconnected. See you next time %(user)s" msgid "You have been disconnected. See you next time %(user)s"
msgstr "" msgstr ""
#: canaille/account.py:138 #: canaille/account.py:143
msgid "Could not send the password initialization link." msgid "Could not send the password initialization link."
msgstr "" msgstr ""
#: canaille/account.py:143 #: canaille/account.py:148
msgid "" msgid ""
"A password initialization link has been sent at your email address. You " "A password initialization link has been sent at your email address. You "
"should receive it within 10 minutes." "should receive it within 10 minutes."
msgstr "" msgstr ""
#: canaille/account.py:149 canaille/account.py:353 #: canaille/account.py:154 canaille/account.py:388
msgid "Could not send the password initialization email" msgid "Could not send the password initialization email"
msgstr "" msgstr ""
#: canaille/account.py:208 canaille/account.py:279 #: canaille/account.py:240 canaille/account.py:314
msgid "User account creation failed." msgid "User account creation failed."
msgstr "" msgstr ""
#: canaille/account.py:229 canaille/account.py:250 #: canaille/account.py:261 canaille/account.py:289
msgid "The invitation link that brought you here was invalid." msgid "The invitation link that brought you here was invalid."
msgstr "" msgstr ""
#: canaille/account.py:236 #: canaille/account.py:268
msgid "The invitation link that brought you here has expired."
msgstr ""
#: canaille/account.py:275
msgid "Your account has already been created." msgid "Your account has already been created."
msgstr "" msgstr ""
#: canaille/account.py:243 #: canaille/account.py:282
msgid "You are already logged in, you cannot create an account." msgid "You are already logged in, you cannot create an account."
msgstr "" msgstr ""
#: canaille/account.py:284 #: canaille/account.py:319
msgid "You account has been created successfuly." msgid "You account has been created successfuly."
msgstr "" msgstr ""
#: canaille/account.py:326 #: canaille/account.py:361
msgid "User account creation succeed." msgid "User account creation succeed."
msgstr "" msgstr ""
#: canaille/account.py:347 #: canaille/account.py:382
msgid "" msgid ""
"A password initialization link has been sent at the user email address. " "A password initialization link has been sent at the user email address. "
"It should be received within 10 minutes." "It should be received within 10 minutes."
msgstr "" msgstr ""
#: canaille/account.py:361 #: canaille/account.py:396
msgid "" msgid ""
"A password reset link has been sent at the user email address. It should " "A password reset link has been sent at the user email address. It should "
"be received within 10 minutes." "be received within 10 minutes."
msgstr "" msgstr ""
#: canaille/account.py:367 #: canaille/account.py:402
msgid "Could not send the password reset email" msgid "Could not send the password reset email"
msgstr "" msgstr ""
#: canaille/account.py:398 #: canaille/account.py:433
msgid "Profile edition failed." msgid "Profile edition failed."
msgstr "" msgstr ""
#: canaille/account.py:426 #: canaille/account.py:461
msgid "Profile updated successfuly." msgid "Profile updated successfuly."
msgstr "" msgstr ""
#: canaille/account.py:446 #: canaille/account.py:481
#, python-format #, python-format
msgid "The user %(user)s has been sucessfuly deleted" msgid "The user %(user)s has been sucessfuly deleted"
msgstr "" msgstr ""
#: canaille/account.py:470 #: canaille/account.py:505
msgid "Could not send the password reset link." msgid "Could not send the password reset link."
msgstr "" msgstr ""
#: canaille/account.py:477 canaille/account.py:488 #: canaille/account.py:512 canaille/account.py:523
msgid "" msgid ""
"A password reset link has been sent at your email address. You should " "A password reset link has been sent at your email address. You should "
"receive it within 10 minutes." "receive it within 10 minutes."
msgstr "" msgstr ""
#: canaille/account.py:494 #: canaille/account.py:529
msgid "Could not reset your password" msgid "Could not reset your password"
msgstr "" msgstr ""
#: canaille/account.py:508 #: canaille/account.py:543
msgid "The password reset link that brought you here was invalid." msgid "The password reset link that brought you here was invalid."
msgstr "" msgstr ""
#: canaille/account.py:517 #: canaille/account.py:552
msgid "Your password has been updated successfuly" msgid "Your password has been updated successfuly"
msgstr "" msgstr ""
@ -176,8 +180,8 @@ msgstr ""
msgid "Login" msgid "Login"
msgstr "" msgstr ""
#: canaille/forms.py:47 canaille/forms.py:71 canaille/forms.py:122 #: canaille/admin/mail.py:29 canaille/forms.py:47 canaille/forms.py:71
#: canaille/forms.py:222 #: canaille/forms.py:122 canaille/forms.py:222
msgid "jane@doe.com" msgid "jane@doe.com"
msgstr "" msgstr ""
@ -297,11 +301,11 @@ msgstr ""
msgid "The group %(group)s has been sucessfully deleted" msgid "The group %(group)s has been sucessfully deleted"
msgstr "" msgstr ""
#: canaille/admin/mail.py:79 canaille/mails.py:25 #: canaille/admin/mail.py:108 canaille/mails.py:25
msgid "Password reset on {website_name}" msgid "Password reset on {website_name}"
msgstr "" msgstr ""
#: canaille/admin/mail.py:37 canaille/mails.py:65 #: canaille/admin/mail.py:66 canaille/mails.py:65
msgid "Password initialization on {website_name}" msgid "Password initialization on {website_name}"
msgstr "" msgstr ""
@ -417,11 +421,19 @@ msgstr ""
msgid "The client has been deleted." msgid "The client has been deleted."
msgstr "" msgstr ""
#: canaille/admin/mail.py:121 #: canaille/admin/mail.py:23 canaille/templates/userlist.html:14
msgid "Email"
msgstr ""
#: canaille/admin/mail.py:42 canaille/admin/mail.py:44
msgid "The test invitation mail has been sent correctly"
msgstr ""
#: canaille/admin/mail.py:150
msgid "Invitation on {website_name}" msgid "Invitation on {website_name}"
msgstr "" msgstr ""
#: canaille/templates/about.html:12 canaille/themes/default/base.html:98 #: canaille/templates/about.html:12 canaille/themes/default/base.html:102
msgid "About canaille" msgid "About canaille"
msgstr "" msgstr ""
@ -558,11 +570,11 @@ msgstr ""
#: canaille/templates/admin/client_edit.html:35 #: canaille/templates/admin/client_edit.html:35
#: canaille/templates/admin/client_edit.html:44 #: canaille/templates/admin/client_edit.html:44
#: canaille/templates/admin/client_edit.html:53 #: canaille/templates/admin/client_edit.html:53
#: canaille/templates/fomanticui.html:62 #: canaille/templates/fomanticui.html:64
msgid "This field is not editable" msgid "This field is not editable"
msgstr "" msgstr ""
#: canaille/templates/fomanticui.html:66 #: canaille/templates/fomanticui.html:68
msgid "This field is required" msgid "This field is required"
msgstr "" msgstr ""
@ -587,6 +599,7 @@ msgstr ""
msgid "Send again" msgid "Send again"
msgstr "" msgstr ""
#: canaille/templates/admin/mails.html:23
#: canaille/templates/forgotten-password.html:40 #: canaille/templates/forgotten-password.html:40
msgid "Send" msgid "Send"
msgstr "" msgstr ""
@ -876,7 +889,7 @@ msgstr ""
msgid "Send mail" msgid "Send mail"
msgstr "" msgstr ""
#: canaille/templates/admin/mails.html:34 canaille/templates/profile.html:221 #: canaille/templates/admin/mails.html:56 canaille/templates/profile.html:221
#: canaille/templates/reset-password.html:11 #: canaille/templates/reset-password.html:11
#: canaille/templates/reset-password.html:16 #: canaille/templates/reset-password.html:16
msgid "Password reset" msgid "Password reset"
@ -904,10 +917,6 @@ msgstr ""
msgid "Invite a user" msgid "Invite a user"
msgstr "" msgstr ""
#: canaille/templates/userlist.html:14
msgid "Email"
msgstr ""
#: canaille/templates/users.html:18 #: canaille/templates/users.html:18
msgid "Add a user" msgid "Add a user"
msgstr "" msgstr ""
@ -987,17 +996,27 @@ msgstr ""
msgid "URL" msgid "URL"
msgstr "" msgstr ""
#: canaille/templates/admin/mails.html:7 #: canaille/templates/admin/mails.html:8
msgid "Mail sending test"
msgstr ""
#: canaille/templates/admin/mails.html:13
msgid ""
"This form will send a fake invitation email to the address you want.\n"
" This should be used for testing mail configuration."
msgstr ""
#: canaille/templates/admin/mails.html:32
msgid "Email preview" msgid "Email preview"
msgstr "" msgstr ""
#: canaille/templates/admin/mails.html:22 #: canaille/templates/admin/mails.html:44
#: canaille/templates/mail/firstlogin.html:19 #: canaille/templates/mail/firstlogin.html:19
#: canaille/templates/mail/reset.txt:1 #: canaille/templates/mail/reset.txt:1
msgid "Password initialization" msgid "Password initialization"
msgstr "" msgstr ""
#: canaille/templates/admin/mails.html:46 #: canaille/templates/admin/mails.html:68
msgid "Invitation" msgid "Invitation"
msgstr "" msgstr ""
@ -1104,6 +1123,10 @@ msgstr ""
msgid "Codes" msgid "Codes"
msgstr "" msgstr ""
#: canaille/themes/default/base.html:84 #: canaille/themes/default/base.html:81
msgid "Emails"
msgstr ""
#: canaille/themes/default/base.html:88
msgid "Log out" msgid "Log out"
msgstr "" msgstr ""

View file

@ -40,6 +40,10 @@ OIDC_METADATA_FILE = "conf/openid-configuration.json"
# wether the login exists or not. # wether the login exists or not.
# HIDE_INVALID_LOGINS = false # HIDE_INVALID_LOGINS = false
# The validity duration of registration invitations, in seconds.
# Defaults to 2 days
# INVITATION_EXPIRATION = 172800
[LOGGING] [LOGGING]
# LEVEL can be one value among: # LEVEL can be one value among:
# DEBUG, INFO, WARNING, ERROR, CRITICAL # DEBUG, INFO, WARNING, ERROR, CRITICAL

View file

@ -51,6 +51,10 @@ Canaille is based on Flask, so any `flask configuration <https://flask.palletspr
*Optional.* Wether to tell the users if a username exists during failing login attempts. *Optional.* Wether to tell the users if a username exists during failing login attempts.
Defaults to ``True``. This may be a security issue to disable this, as this give a way to malicious people to guess who has an account on this canaille instance. Defaults to ``True``. This may be a security issue to disable this, as this give a way to malicious people to guess who has an account on this canaille instance.
:INVITATION_EXPIRATION:
*Optional* The validity duration of registration invitations, in seconds.
Defaults to 2 days.
LOGGING LOGGING
------- -------

View file

@ -36,6 +36,7 @@ install_requires =
sentry-sdk[flask] sentry-sdk[flask]
toml toml
wtforms wtforms
dataclasses;python_version<'3.7'
[options.packages.find] [options.packages.find]
exclude = exclude =

View file

@ -1,3 +1,8 @@
from datetime import datetime
from datetime import timedelta
from canaille.account import Invitation
from canaille.apputils import b64_to_obj
from canaille.apputils import obj_to_b64 from canaille.apputils import obj_to_b64
from canaille.apputils import profile_hash from canaille.apputils import profile_hash
from canaille.models import User from canaille.models import User
@ -37,7 +42,10 @@ def test_invitation(testclient, slapd_connection, logged_admin, foo_group, smtpd
with testclient.app.app_context(): with testclient.app.app_context():
user = User.get("someone", conn=slapd_connection) user = User.get("someone", conn=slapd_connection)
user.load_groups(conn=slapd_connection)
foo_group.reload(slapd_connection)
assert user.check_password("whatever") assert user.check_password("whatever")
assert user.groups == [foo_group]
with testclient.session_transaction() as sess: with testclient.session_transaction() as sess:
assert "user_dn" in sess assert "user_dn" in sess
@ -80,7 +88,10 @@ def test_generate_link(testclient, slapd_connection, logged_admin, foo_group, sm
with testclient.app.app_context(): with testclient.app.app_context():
user = User.get("sometwo", conn=slapd_connection) user = User.get("sometwo", conn=slapd_connection)
user.load_groups(conn=slapd_connection)
foo_group.reload(slapd_connection)
assert user.check_password("whatever") assert user.check_password("whatever")
assert user.groups == [foo_group]
with testclient.session_transaction() as sess: with testclient.session_transaction() as sess:
assert "user_dn" in sess assert "user_dn" in sess
@ -100,37 +111,86 @@ def test_invitation_login_already_taken(testclient, slapd_connection, logged_adm
assert "The email &#39;jane@doe.com&#39; already exists" in res.text assert "The email &#39;jane@doe.com&#39; already exists" in res.text
def test_registration_invalid_data(testclient, slapd_connection, foo_group): def test_registration(testclient, slapd_connection, foo_group):
with testclient.app.app_context(): with testclient.app.app_context():
data = ["someoneelse", "someone@mydomain.tld", foo_group.dn] invitation = Invitation(
b64 = obj_to_b64(data) datetime.now().isoformat(),
"someoneelse",
"someone@mydomain.tld",
foo_group.dn,
)
b64 = invitation.b64()
hash = invitation.profile_hash()
testclient.get(f"/register/{b64}/invalid", status=302) testclient.get(f"/register/{b64}/{hash}", status=200)
def test_registration_invalid_hash(testclient, slapd_connection, foo_group): def test_registration_invalid_hash(testclient, slapd_connection, foo_group):
with testclient.app.app_context(): with testclient.app.app_context():
data = ["someoneelse", "someone@mydomain.tld", foo_group.dn] invitation = Invitation(
hash = profile_hash(*data) datetime.now().isoformat(),
data = ["anything", "someone@mydomain.tld", foo_group.dn] "someoneelse",
b64 = obj_to_b64(data) "someone@mydomain.tld",
foo_group.dn,
)
b64 = invitation.b64()
testclient.get(f"/register/{b64}/{hash}", status=302) testclient.get(f"/register/{b64}/invalid", status=302)
def test_registration_bad_hash(testclient, slapd_connection, foo_group): def test_registration_bad_hash(testclient, slapd_connection, foo_group):
with testclient.app.app_context(): with testclient.app.app_context():
data = ["someoneelse", "someone@mydomain.tld", foo_group.dn] now = datetime.now().isoformat()
hash = profile_hash(*data) invitation1 = Invitation(
now, "someoneelse", "someone@mydomain.tld", foo_group.dn
)
hash = invitation1.profile_hash()
invitation2 = Invitation(now, "anything", "someone@mydomain.tld", foo_group.dn)
b64 = invitation2.b64()
testclient.get(f"/register/invalid/{hash}", status=302) testclient.get(f"/register/{b64}/{hash}", status=302)
def test_registration_invalid_data(testclient, slapd_connection, foo_group):
with testclient.app.app_context():
invitation = Invitation(
datetime.now().isoformat(),
"someoneelse",
"someone@mydomain.tld",
foo_group.dn,
)
hash = invitation.profile_hash()
res = testclient.get(f"/register/invalid/{hash}", status=302)
def test_registration_more_than_48_hours_after_invitation(
testclient, slapd_connection, foo_group
):
with testclient.app.app_context():
two_days_ago = datetime.now() - timedelta(hours=48)
invitation = Invitation(
two_days_ago.isoformat(),
"someoneelse",
"someone@mydomain.tld",
foo_group.dn,
)
hash = invitation.profile_hash()
b64 = invitation.b64()
testclient.get(f"/register/{b64}/{hash}", status=302)
def test_registration_no_password(testclient, slapd_connection, foo_group): def test_registration_no_password(testclient, slapd_connection, foo_group):
with testclient.app.app_context(): with testclient.app.app_context():
data = ["someoneelse", "someone@mydomain.tld", foo_group.dn] invitation = Invitation(
hash = profile_hash(*data) datetime.now().isoformat(),
b64 = obj_to_b64(data) "someoneelse",
"someone@mydomain.tld",
foo_group.dn,
)
hash = invitation.profile_hash()
b64 = invitation.b64()
url = f"/register/{b64}/{hash}" url = f"/register/{b64}/{hash}"
res = testclient.get(url, status=200) res = testclient.get(url, status=200)
@ -151,9 +211,14 @@ def test_no_registration_if_logged_in(
testclient, slapd_connection, logged_user, foo_group testclient, slapd_connection, logged_user, foo_group
): ):
with testclient.app.app_context(): with testclient.app.app_context():
data = ["anyone", "someone@mydomain.tld", foo_group.dn] invitation = Invitation(
hash = profile_hash(*data) datetime.now().isoformat(),
b64 = obj_to_b64(data) "someoneelse",
"someone@mydomain.tld",
foo_group.dn,
)
hash = invitation.profile_hash()
b64 = invitation.b64()
url = f"/register/{b64}/{hash}" url = f"/register/{b64}/{hash}"
testclient.get(url, status=302) testclient.get(url, status=302)