diff --git a/.gitignore b/.gitignore index b26f8a3a..e9d0a903 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ canaille/conf/openid-configuration.json canaille/conf/*.pem canaille/conf/*.pub canaille/conf/*.key +.vscode \ No newline at end of file diff --git a/canaille/__init__.py b/canaille/__init__.py index 342cc6c8..48e9792e 100644 --- a/canaille/__init__.py +++ b/canaille/__init__.py @@ -12,6 +12,7 @@ import canaille.consents import canaille.commands.clean import canaille.oauth import canaille.account +import canaille.groups import canaille.well_known from cryptography.hazmat.primitives import serialization as crypto_serialization @@ -136,6 +137,7 @@ def setup_app(app): config_oauth(app) setup_ldap_tree(app) app.register_blueprint(canaille.account.bp) + app.register_blueprint(canaille.groups.bp, url_prefix="/groups") app.register_blueprint(canaille.oauth.bp, url_prefix="/oauth") app.register_blueprint(canaille.commands.clean.bp) app.register_blueprint(canaille.consents.bp, url_prefix="/consent") diff --git a/canaille/conf/oauth-authorization-server.sample.json b/canaille/conf/oauth-authorization-server.sample.json index b365b625..7e4565c6 100644 --- a/canaille/conf/oauth-authorization-server.sample.json +++ b/canaille/conf/oauth-authorization-server.sample.json @@ -18,7 +18,7 @@ "https://mydomain.tld/oauth/register", "scopes_supported": ["openid", "profile", "email", "address", - "phone"], + "phone", "groups"], "response_types_supported": ["code", "token", "id_token", "code token", "code id_token", "token id_token"], diff --git a/canaille/conf/openid-configuration.sample.json b/canaille/conf/openid-configuration.sample.json index bca02049..f4986470 100644 --- a/canaille/conf/openid-configuration.sample.json +++ b/canaille/conf/openid-configuration.sample.json @@ -22,7 +22,7 @@ "https://mydomain.tld/oauth/register", "scopes_supported": ["openid", "profile", "email", "address", - "phone"], + "phone", "groups"], "response_types_supported": ["code", "token", "id_token", "code token", "code id_token", "token id_token"], diff --git a/canaille/forms.py b/canaille/forms.py index e57f11e1..8e0a4de0 100644 --- a/canaille/forms.py +++ b/canaille/forms.py @@ -145,3 +145,19 @@ def profile_form(field_names): render_kw={}, ) return wtforms.form.BaseForm(fields) + + +class GroupForm(FlaskForm): + name = wtforms.StringField( + _("Name"), + validators=[wtforms.validators.DataRequired()], + render_kw={ + "placeholder": _("group"), + }, + ) + + def validate_name(self, field): + if Group.get(field.data): + raise wtforms.ValidationError( + _("The group '{group}' already exists").format(group=field.data) + ) diff --git a/canaille/groups.py b/canaille/groups.py new file mode 100644 index 00000000..0c7882f9 --- /dev/null +++ b/canaille/groups.py @@ -0,0 +1,89 @@ +from flask import ( + Blueprint, + render_template, + redirect, + url_for, + request, + flash, + current_app, + abort, +) +from flask_babel import gettext as _ + +from .flaskutils import moderator_needed +from .forms import GroupForm +from .models import Group + +bp = Blueprint("groups", __name__) + + +@bp.route("/") +@moderator_needed() +def groups(user): + groups = Group.filter(objectClass=current_app.config["LDAP"]["GROUP_CLASS"]) + return render_template("groups.html", groups=groups, menuitem="groups") + + +@bp.route("/add", methods=("GET", "POST")) +@moderator_needed() +def create_group(user): + form = GroupForm(request.form or None) + try: + if "name" in form: + del form["name"].render_kw["disabled"] + except KeyError: + pass + + if request.form: + if not form.validate(): + flash(_("Group creation failed."), "error") + else: + group = Group(objectClass=current_app.config["LDAP"]["GROUP_CLASS"]) + group.member = [user.dn] + group.cn = [form.name.data] + group.save() + flash( + _("The group %(group)s has been sucessfully created", group=group.name), + "success", + ) + return redirect(url_for("groups.group", groupname=group.name)) + + return render_template("group.html", form=form, edited_group=None, members=None) + + +@bp.route("/", methods=("GET", "POST")) +@moderator_needed() +def group(user, groupname): + group = Group.get(groupname) or abort(404) + + if request.method == "GET" or request.form.get("action") == "edit": + return edit_group(group) + + if request.form.get("action") == "delete": + return delete_group(group) + + abort(400) + + +def edit_group(group): + form = GroupForm(request.form or None, data={"name": group.name}) + form["name"].render_kw["disabled"] = "true" + + if request.form: + if form.validate(): + group.save() + else: + flash(_("Group edition failed."), "error") + + return render_template( + "group.html", form=form, edited_group=group, members=group.get_members() + ) + + +def delete_group(group): + flash( + _("The group %(group)s has been sucessfully deleted", group=group.name), + "success", + ) + group.delete() + return redirect(url_for("groups.groups")) diff --git a/canaille/models.py b/canaille/models.py index 2adbf151..ba1f4665 100644 --- a/canaille/models.py +++ b/canaille/models.py @@ -160,6 +160,11 @@ class Group(LDAPObject): Group.attr_type_by_name(conn=conn) return [(group[attribute][0], group.dn) for group in groups] + @property + def name(self): + attribute = current_app.config["LDAP"].get("GROUP_NAME_ATTRIBUTE") + return self[attribute][0] + def get_members(self, conn=None): return [User.get(dn=user_dn, conn=conn) for user_dn in self.member] diff --git a/canaille/oauth.py b/canaille/oauth.py index 978fc3ee..de3b24cc 100644 --- a/canaille/oauth.py +++ b/canaille/oauth.py @@ -27,6 +27,7 @@ CLAIMS = { "email": ("at", _("Your email address.")), "address": ("envelope open outline", _("Your postal address.")), "phone": ("phone", _("Your phone number.")), + "groups": ("users", _("Groups you are belonging to")), } diff --git a/canaille/oauth2utils.py b/canaille/oauth2utils.py index 856ef6ed..481bdb95 100644 --- a/canaille/oauth2utils.py +++ b/canaille/oauth2utils.py @@ -61,6 +61,8 @@ def generate_user_info(user, scope): fields += ["address"] if "phone" in scope: fields += ["phone_number", "phone_number_verified"] + if "groups" in scope: + fields += ["groups"] data = {} for field in fields: @@ -69,6 +71,9 @@ def generate_user_info(user, scope): data[field] = user.__getattr__(ldap_field_match) if isinstance(data[field], list): data[field] = data[field][0] + if field == "groups": + group_name_attr = current_app.config["LDAP"]["GROUP_NAME_ATTRIBUTE"] + data[field] = [getattr(g, group_name_attr)[0] for g in user.groups] return UserInfo(**data) diff --git a/canaille/static/js/profile.js b/canaille/static/js/confirm.js similarity index 100% rename from canaille/static/js/profile.js rename to canaille/static/js/confirm.js diff --git a/canaille/templates/about.html b/canaille/templates/about.html index a369d4cd..d631471b 100644 --- a/canaille/templates/about.html +++ b/canaille/templates/about.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% import 'fomanticui.j2' as sui %} -{% import 'flask.j2' as flask %} {% block content %}
@@ -8,7 +7,6 @@ {{ website_name }} - {{ flask.messages() }}

{{ _("About canaille") }} diff --git a/canaille/templates/admin/authorization_view.html b/canaille/templates/admin/authorization_view.html index 11192da5..3424a132 100644 --- a/canaille/templates/admin/authorization_view.html +++ b/canaille/templates/admin/authorization_view.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% import 'fomanticui.j2' as sui %} -{% import 'flask.j2' as flask %} {% block content %}
@@ -8,8 +7,6 @@ {% trans %}View a authorization{% endtrans %}

- {{ flask.messages() }} -
    {% for attr in authorization.may %} diff --git a/canaille/templates/admin/client_add.html b/canaille/templates/admin/client_add.html index de08ce21..e628f4e2 100644 --- a/canaille/templates/admin/client_add.html +++ b/canaille/templates/admin/client_add.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% import 'fomanticui.j2' as sui %} -{% import 'flask.j2' as flask %} {% block content %}
    @@ -8,8 +7,6 @@ {% trans %}Add a client{% endtrans %} - {{ flask.messages() }} -
    {{ sui.render_form(form, _("Confirm")) }}
    diff --git a/canaille/templates/admin/client_edit.html b/canaille/templates/admin/client_edit.html index 71d18ced..9edd16e3 100644 --- a/canaille/templates/admin/client_edit.html +++ b/canaille/templates/admin/client_edit.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% import 'fomanticui.j2' as sui %} -{% import 'flask.j2' as flask %} {% block script %} @@ -26,8 +25,6 @@ {% trans %}Edit a client{% endtrans %} - {{ flask.messages() }} -
    diff --git a/canaille/templates/admin/client_list.html b/canaille/templates/admin/client_list.html index b4a61da4..0152d9a6 100644 --- a/canaille/templates/admin/client_list.html +++ b/canaille/templates/admin/client_list.html @@ -1,5 +1,4 @@ {% extends 'base.html' %} -{% import 'flask.j2' as flask %} {% block style %} @@ -14,8 +13,6 @@ {% block content %} -{{ flask.messages() }} - diff --git a/canaille/templates/admin/token_view.html b/canaille/templates/admin/token_view.html index b7d567b4..2d8980c1 100644 --- a/canaille/templates/admin/token_view.html +++ b/canaille/templates/admin/token_view.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% import 'fomanticui.j2' as sui %} -{% import 'flask.j2' as flask %} {% block content %}
    @@ -8,8 +7,6 @@ {% trans %}View a token{% endtrans %} - {{ flask.messages() }} -
      {% for attr in token.may %} diff --git a/canaille/templates/base.html b/canaille/templates/base.html index fabd8afe..4d8198dd 100644 --- a/canaille/templates/base.html +++ b/canaille/templates/base.html @@ -1,3 +1,5 @@ +{% import 'flask.j2' as flask %} + @@ -47,6 +49,11 @@ {% trans %}Users{% endtrans %} + + + {% trans %}Groups{% endtrans %} + {% endif %} {% if user.admin %} - {{ flask.messages() }} - {% if consents %}
      {% for consent in consents %} diff --git a/canaille/templates/firstlogin.html b/canaille/templates/firstlogin.html index 4c9103f3..b9cb948f 100644 --- a/canaille/templates/firstlogin.html +++ b/canaille/templates/firstlogin.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% import 'fomanticui.j2' as sui %} -{% import 'flask.j2' as flask %} {% block content %}
      @@ -13,8 +12,6 @@
      - {{ flask.messages() }} -
      {% trans %} It seems this is the first time you are logging here. In order to finalize your diff --git a/canaille/templates/forgotten-password.html b/canaille/templates/forgotten-password.html index 12b6bf2d..2d5bb676 100644 --- a/canaille/templates/forgotten-password.html +++ b/canaille/templates/forgotten-password.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% import 'fomanticui.j2' as sui %} -{% import 'flask.j2' as flask %} {% block content %}
      @@ -13,8 +12,6 @@
      - {{ flask.messages() }} -
      {% trans %} After this form is sent, if the email address or the login you provided diff --git a/canaille/templates/group.html b/canaille/templates/group.html new file mode 100644 index 00000000..a5973597 --- /dev/null +++ b/canaille/templates/group.html @@ -0,0 +1,87 @@ +{% extends 'base.html' %} +{% import 'fomanticui.j2' as sui %} + +{% block script %} + +{% endblock %} + +{% block content %} +{% if edited_group %} + +{% endif %} + +{% if edited_group %} +
      +

      {% trans %}Members{% endtrans %}

      + +
      +{% endif %} + +
      +

      +
      + {% if not edited_group %} + {% trans %}Group creation{% endtrans %} + {% else %} + {% trans %}Group edition{% endtrans %} + {% endif %} +
      + +
      + {% if not edited_group %} + {% trans %}Create a new group{% endtrans %} + {% else %} + {% trans %}Edit informations about a group{% endtrans %} + {% endif %} +
      +

      + + + {{ form.hidden_tag() if form.hidden_tag }} + {{ sui.render_field(form.name) }} + {% if not edited_group %} +

      + {% trans %}Because group cannot be empty, you will be added to the group. You can remove you later by editing your profile when you will have added other members to the group.{% endtrans %} +

      + {% endif %} + + {% if edited_group %} + + {% endif %} + +
      +{% endblock %} diff --git a/canaille/templates/groups.html b/canaille/templates/groups.html new file mode 100644 index 00000000..94d87286 --- /dev/null +++ b/canaille/templates/groups.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block content %} + + +
      +

      {% trans %}Groups{% endtrans %}

      + +
      +{% endblock %} \ No newline at end of file diff --git a/canaille/templates/login.html b/canaille/templates/login.html index 02e962eb..1f745128 100644 --- a/canaille/templates/login.html +++ b/canaille/templates/login.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% import 'fomanticui.j2' as sui %} -{% import 'flask.j2' as flask %} {% block content %}
      @@ -19,8 +18,6 @@
      {% trans %}Log-in and manage your authorizations.{% endtrans %}
      - {{ flask.messages() }} -
      @@ -19,8 +18,6 @@
      {% trans %}Please enter your password for this account.{% endtrans %}
      - {{ flask.messages() }} - + {% endblock %} {% block content %} @@ -52,8 +51,6 @@
      - {{ flask.messages() }} - @@ -13,8 +12,6 @@
      - {{ flask.messages() }} -
      {{ sui.render_form(form, _("Password reset"), action=url_for("account.reset", uid=uid, hash=hash)) }}
      diff --git a/canaille/templates/users.html b/canaille/templates/users.html index 3669b215..3adb6888 100644 --- a/canaille/templates/users.html +++ b/canaille/templates/users.html @@ -12,7 +12,6 @@ {% endblock %} {% block content %} - diff --git a/canaille/translations/fr/LC_MESSAGES/messages.mo b/canaille/translations/fr/LC_MESSAGES/messages.mo index 480fc2e9..04f17562 100644 Binary files a/canaille/translations/fr/LC_MESSAGES/messages.mo and b/canaille/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/canaille/translations/fr/LC_MESSAGES/messages.po b/canaille/translations/fr/LC_MESSAGES/messages.po index 20f6e4f8..dd17555b 100644 --- a/canaille/translations/fr/LC_MESSAGES/messages.po +++ b/canaille/translations/fr/LC_MESSAGES/messages.po @@ -3,24 +3,25 @@ # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2020. # Éloi Rivard , 2020-2021. +# Camille , 2021. # msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: contact@yaal.fr\n" -"POT-Creation-Date: 2021-06-03 15:21+0200\n" -"PO-Revision-Date: 2021-06-03 15:22+0200\n" -"Last-Translator: Éloi Rivard \n" +"POT-Creation-Date: 2021-07-29 16:09+0200\n" +"PO-Revision-Date: 2021-07-29 16:31+0200\n" +"Last-Translator: Camille \n" "Language: fr\n" -"Language-Team: French \n" +"Language-Team: French \n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" -"X-Generator: Gtranslator 40.0\n" +"X-Generator: Gtranslator 3.38.0\n" -#: canaille/account.py:62 canaille/account.py:87 canaille/oauth.py:58 +#: canaille/account.py:62 canaille/account.py:87 canaille/oauth.py:59 msgid "Login failed, please check your information" msgstr "La connexion a échoué, veuillez vérifier vos informations." @@ -155,9 +156,9 @@ msgstr "Identifiant" msgid "jdoe" msgstr "mdupont" -#: canaille/admin/clients.py:23 canaille/forms.py:82 -#: canaille/templates/admin/client_list.html:25 -#: canaille/templates/users.html:22 +#: canaille/admin/clients.py:23 canaille/forms.py:82 canaille/forms.py:152 +#: canaille/templates/admin/client_list.html:22 +#: canaille/templates/users.html:21 msgid "Name" msgstr "Nom" @@ -181,7 +182,7 @@ msgstr "Dupont" msgid "Email address" msgstr "Courriel" -#: canaille/forms.py:109 canaille/templates/users.html:24 +#: canaille/forms.py:109 canaille/templates/users.html:23 msgid "Phone number" msgstr "Numéro de téléphone" @@ -201,10 +202,37 @@ msgstr "Numéro" msgid "1234" msgstr "1234" -#: canaille/forms.py:143 +#: canaille/forms.py:143 canaille/templates/base.html:55 +#: canaille/templates/groups.html:9 msgid "Groups" msgstr "Groupes" +#: canaille/forms.py:155 +msgid "group" +msgstr "groupe" + +#: canaille/forms.py:162 +msgid "The group '{group}' already exists" +msgstr "Le group '{group}' existe déjà" + +#: canaille/groups.py:39 +msgid "Group creation failed." +msgstr "La création du groupe a échoué." + +#: canaille/groups.py:46 +#, python-format +msgid "The group %(group)s has been sucessfully created" +msgstr "Le groupe %(group)s a bien été créé" + +#: canaille/groups.py:76 +msgid "Group edition failed." +msgstr "L'édition du groupe a échoué." + +#: canaille/groups.py:85 +#, python-format +msgid "The group %(group)s has been sucessfully deleted" +msgstr "Le groupe %(group)s a bien été supprimé" + #: canaille/admin/mail.py:27 canaille/mails.py:27 msgid "Password reset on {website_name}" msgstr "Réinitialisation du mot de passe sur {website_name}" @@ -229,7 +257,11 @@ msgstr "Votre adresse postale." msgid "Your phone number." msgstr "Votre numéro de téléphone." -#: canaille/oauth.py:97 +#: canaille/oauth.py:30 +msgid "Groups you are belonging to" +msgstr "Les groupes dans lesquels vous êtes" + +#: canaille/oauth.py:98 msgid "You have been successfully logged out." msgstr "Vous avez été déconnectés." @@ -309,24 +341,24 @@ msgstr "Le client a été édité." msgid "The client has been deleted." msgstr "Le client a été supprimé." -#: canaille/templates/about.html:14 canaille/templates/base.html:90 +#: canaille/templates/about.html:12 canaille/templates/base.html:98 msgid "About canaille" msgstr "À propos de canaille" -#: canaille/templates/about.html:16 +#: canaille/templates/about.html:14 msgid "Free and open-source identity provider." msgstr "Fournisseur d'identité numérique libre" -#: canaille/templates/about.html:19 +#: canaille/templates/about.html:17 #, python-format msgid "Version %(version)s" msgstr "Version %(version)s" -#: canaille/templates/about.html:20 +#: canaille/templates/about.html:18 msgid "Source code" msgstr "Code source" -#: canaille/templates/about.html:21 +#: canaille/templates/about.html:19 msgid "Documentation" msgstr "Documentation" @@ -352,67 +384,67 @@ msgstr "Changer d'utilisateur" msgid "Accept" msgstr "Accepter" -#: canaille/templates/base.html:8 +#: canaille/templates/base.html:10 msgid "authorization interface" msgstr " - Interface de gestion des autorisations" -#: canaille/templates/base.html:37 canaille/templates/profile.html:38 +#: canaille/templates/base.html:39 canaille/templates/profile.html:37 msgid "My profile" msgstr "Mon profil" -#: canaille/templates/base.html:42 canaille/templates/consent_list.html:19 +#: canaille/templates/base.html:44 canaille/templates/consent_list.html:18 msgid "My consents" msgstr "Mes autorisations" -#: canaille/templates/base.html:48 +#: canaille/templates/base.html:50 msgid "Users" msgstr "Utilisateurs" -#: canaille/templates/base.html:58 +#: canaille/templates/base.html:65 msgid "Clients" msgstr "Clients" -#: canaille/templates/base.html:62 +#: canaille/templates/base.html:69 msgid "Tokens" msgstr "Jetons" -#: canaille/templates/base.html:66 +#: canaille/templates/base.html:73 msgid "Codes" msgstr "Codes" -#: canaille/templates/base.html:70 +#: canaille/templates/base.html:77 msgid "Consents" msgstr "Autorisations" -#: canaille/templates/base.html:77 +#: canaille/templates/base.html:84 msgid "Log out" msgstr "Déconnexion" -#: canaille/templates/consent_list.html:22 +#: canaille/templates/consent_list.html:21 msgid "Consult and revoke the authorization you gave to websites." msgstr "Consultez et révoquez les autorisation que vous avez données." -#: canaille/templates/consent_list.html:42 +#: canaille/templates/consent_list.html:39 msgid "From:" msgstr "À partir de :" -#: canaille/templates/consent_list.html:44 +#: canaille/templates/consent_list.html:41 msgid "Revoked:" msgstr "Révoqué le :" -#: canaille/templates/consent_list.html:47 +#: canaille/templates/consent_list.html:44 msgid "Has access to:" msgstr "A accès à :" -#: canaille/templates/consent_list.html:57 +#: canaille/templates/consent_list.html:54 msgid "Remove access" msgstr "Supprimer l'accès" -#: canaille/templates/consent_list.html:67 +#: canaille/templates/consent_list.html:64 msgid "Nothing here" msgstr "Rien ici" -#: canaille/templates/consent_list.html:68 +#: canaille/templates/consent_list.html:65 msgid "You did not authorize applications yet." msgstr "" "Vous n'avez pas encore autorisé d'application à accéder à votre profil." @@ -433,11 +465,11 @@ msgstr "Page non trouvée" msgid "Technical problem" msgstr "Problème technique" -#: canaille/templates/firstlogin.html:12 +#: canaille/templates/firstlogin.html:11 msgid "First login" msgstr "Première connexion" -#: canaille/templates/firstlogin.html:19 +#: canaille/templates/firstlogin.html:16 msgid "" "\n" " It seems this is the first time you are logging here. In order to " @@ -459,21 +491,21 @@ msgstr "" " Veuillez cliquer sur le bouton bleu ci-dessous pour envoyer le " "courriel." -#: canaille/templates/firstlogin.html:35 +#: canaille/templates/firstlogin.html:32 msgid "Send the initialization email" msgstr "Envoyer le courriel d'initialisation" -#: canaille/templates/firstlogin.html:36 -#: canaille/templates/forgotten-password.html:43 +#: canaille/templates/firstlogin.html:33 +#: canaille/templates/forgotten-password.html:40 msgid "Login page" msgstr "Page de connexion" -#: canaille/templates/forgotten-password.html:12 -#: canaille/templates/login.html:38 canaille/templates/password.html:35 +#: canaille/templates/forgotten-password.html:11 +#: canaille/templates/login.html:35 canaille/templates/password.html:32 msgid "Forgotten password" msgstr "Mot de passe oublié" -#: canaille/templates/forgotten-password.html:19 +#: canaille/templates/forgotten-password.html:16 msgid "" "\n" " After this form is sent, if the email address or the login you " @@ -490,51 +522,119 @@ msgstr "" " vous permettra de ré-initialiser votre mot de passe.\n" " " -#: canaille/templates/forgotten-password.html:38 -#: canaille/templates/profile.html:120 canaille/templates/profile.html:143 +#: canaille/templates/forgotten-password.html:35 +#: canaille/templates/profile.html:117 canaille/templates/profile.html:140 msgid "Send again" msgstr "Envoyer à nouveau" -#: canaille/templates/forgotten-password.html:40 +#: canaille/templates/forgotten-password.html:37 msgid "Send" msgstr "Envoyer" -#: canaille/templates/login.html:17 +#: canaille/templates/group.html:13 +msgid "Group deletion" +msgstr "Suppression d'un groupe" + +#: canaille/templates/group.html:17 +msgid "" +"Are you sure you want to delete this group? This action is unrevokable and " +"all the data about this group will be removed." +msgstr "" +"Êtes-vous sûrs de vouloir supprimer ce groupe ? Cette action est " +"irrévocable, et toutes les données de cet utilisateur seront supprimées." + +#: canaille/templates/admin/client_edit.html:18 +#: canaille/templates/group.html:21 canaille/templates/profile.html:25 +msgid "Cancel" +msgstr "Annuler" + +#: canaille/templates/admin/client_edit.html:19 +#: canaille/templates/group.html:22 canaille/templates/profile.html:26 +msgid "Delete" +msgstr "Supprimer" + +#: canaille/templates/group.html:29 +msgid "Members" +msgstr "Membres" + +#: canaille/templates/group.html:45 +msgid "Group creation" +msgstr "Nouveau groupe" + +#: canaille/templates/group.html:47 +msgid "Group edition" +msgstr "Édition d'un groupe" + +#: canaille/templates/group.html:53 +msgid "Create a new group" +msgstr "Création d'un nouveau groupe" + +#: canaille/templates/group.html:55 +msgid "Edit informations about a group" +msgstr "Éditez les informations d'un groupe" + +#: canaille/templates/group.html:70 +msgid "" +"Because group cannot be empty, you will be added to the group. You can " +"remove you later by editing your profile when you will have added other " +"members to the group." +msgstr "" +"Vous serez ajouté au groupe car le groupe ne peut pas être vide. Vous " +"pourrez vous retirer du groupe depuis l'édition de votre profil dès que vous " +"aurez ajouter d'autres membres dans le groupe." + +#: canaille/templates/group.html:75 +msgid "Create group" +msgstr "Créer le groupe" + +#: canaille/templates/group.html:77 canaille/templates/profile.html:154 +msgid "Submit" +msgstr "Valider" + +#: canaille/templates/group.html:82 +msgid "Delete group" +msgstr "Supprimer le groupe" + +#: canaille/templates/groups.html:5 +msgid "Add a group" +msgstr "Ajouter un groupe" + +#: canaille/templates/login.html:16 #, python-format msgid "Sign in at %(website)s" msgstr "Connexion à %(website)s" -#: canaille/templates/login.html:19 +#: canaille/templates/login.html:18 msgid "Log-in and manage your authorizations." msgstr "Connectez-vous et gérez vos autorisations." -#: canaille/templates/login.html:37 +#: canaille/templates/login.html:34 msgid "Continue" msgstr "Continuer" -#: canaille/templates/password.html:17 +#: canaille/templates/password.html:16 #, python-format msgid "Sign in as %(username)s" msgstr "Connexion en tant que %(username)s" -#: canaille/templates/password.html:19 +#: canaille/templates/password.html:18 msgid "Please enter your password for this account." msgstr "Veuillez entre votre mot de passe pour ce compte." -#: canaille/templates/password.html:34 +#: canaille/templates/password.html:31 msgid "Sign in" msgstr "Se connecter" -#: canaille/templates/password.html:36 +#: canaille/templates/password.html:33 #, python-format msgid "I am not %(username)s" msgstr "Je ne suis pas %(username)s" -#: canaille/templates/profile.html:14 +#: canaille/templates/profile.html:13 msgid "Account deletion" msgstr "Suppression d'un compte" -#: canaille/templates/profile.html:19 +#: canaille/templates/profile.html:18 msgid "" "Are you sure you want to delete this user? This action is unrevokable and " "all the data about this user will be removed." @@ -542,7 +642,7 @@ msgstr "" "Êtes-vous sûrs de vouloir supprimer cet utilisateur ? Cette action est " "irrévocable, et toutes les données de cet utilisateur seront supprimées." -#: canaille/templates/profile.html:21 +#: canaille/templates/profile.html:20 msgid "" "Are you sure you want to delete your account? This action is unrevokable and " "all your data will be removed forever." @@ -550,57 +650,47 @@ msgstr "" "Êtes-vous sûrs de vouloir supprimer votre compte ? Cette action est " "irrévocable et toutes vos données seront supprimées pour toujours." -#: canaille/templates/admin/client_edit.html:19 -#: canaille/templates/profile.html:26 -msgid "Cancel" -msgstr "Annuler" - -#: canaille/templates/admin/client_edit.html:20 -#: canaille/templates/profile.html:27 -msgid "Delete" -msgstr "Supprimer" - -#: canaille/templates/profile.html:36 +#: canaille/templates/profile.html:35 msgid "User creation" msgstr "Nouvel utilisateur" -#: canaille/templates/profile.html:40 +#: canaille/templates/profile.html:39 msgid "User profile edition" msgstr "Édition d'un profil utilisateur" -#: canaille/templates/profile.html:46 +#: canaille/templates/profile.html:45 msgid "Create a new user account" msgstr "Création d'un nouveau compte utilisateur" -#: canaille/templates/profile.html:48 +#: canaille/templates/profile.html:47 msgid "Edit your personal informations" msgstr "Éditez vos informations personnelles" -#: canaille/templates/profile.html:50 +#: canaille/templates/profile.html:49 msgid "Edit informations about an user" msgstr "Éditez les informations d'un utilisateur" -#: canaille/templates/profile.html:67 +#: canaille/templates/profile.html:64 msgid "Personal information" msgstr "Informations personnelles" -#: canaille/templates/profile.html:89 +#: canaille/templates/profile.html:86 msgid "Account information" msgstr "Informations sur le compte" -#: canaille/templates/profile.html:105 +#: canaille/templates/profile.html:102 msgid "User password is not mandatory" msgstr "Le mot de passe utilisateur n'est pas requis à la création" -#: canaille/templates/profile.html:107 +#: canaille/templates/profile.html:104 msgid "The user password can be set:" msgstr "Il pourra être renseigné :" -#: canaille/templates/profile.html:109 +#: canaille/templates/profile.html:106 msgid "by filling this form;" msgstr "en remplissant ce formulaire ;" -#: canaille/templates/profile.html:110 +#: canaille/templates/profile.html:107 msgid "" "by sending the user a password initialization mail, after the account " "creation;" @@ -608,7 +698,7 @@ msgstr "" "en envoyant un lien d'initialisation de mot de passe, par mail à " "l'utilisateur, après la création de son compte;" -#: canaille/templates/profile.html:111 canaille/templates/profile.html:134 +#: canaille/templates/profile.html:108 canaille/templates/profile.html:131 msgid "" "or simply waiting for the user to sign-in a first time, and then receive a " "password initialization mail." @@ -616,40 +706,40 @@ msgstr "" "ou simplement en attendant la première connexion de l'utilisateur, afin " "qu'il reçoive un lien d'initialisation de mot de passe par email." -#: canaille/templates/profile.html:124 +#: canaille/templates/profile.html:121 msgid "Send email" msgstr "Envoyer l'email" -#: canaille/templates/profile.html:128 +#: canaille/templates/profile.html:125 msgid "This user does not have a password yet" msgstr "L'utilisateur n'a pas encore de mot de passe" -#: canaille/templates/profile.html:130 +#: canaille/templates/profile.html:127 msgid "You can solve this by:" msgstr "Vous pouvez régler ceci en :" -#: canaille/templates/profile.html:132 +#: canaille/templates/profile.html:129 msgid "setting a password using this form;" msgstr "renseignant un mot de passe via ce formulaire ;" -#: canaille/templates/profile.html:133 +#: canaille/templates/profile.html:130 msgid "" "sending the user a password initialization mail, by clicking this button;" msgstr "" "envoyant un lien d'initialisation de mot de passe, par mail à l'utilisateur, " "en cliquant sur ce bouton;" -#: canaille/templates/profile.html:145 +#: canaille/templates/profile.html:142 msgid "Send mail" msgstr "Envoyer l'email" -#: canaille/templates/profile.html:149 -#: canaille/templates/reset-password.html:12 -#: canaille/templates/reset-password.html:19 +#: canaille/templates/profile.html:146 +#: canaille/templates/reset-password.html:11 +#: canaille/templates/reset-password.html:16 msgid "Password reset" msgstr "Réinitialisation du mot de passe" -#: canaille/templates/profile.html:151 +#: canaille/templates/profile.html:148 msgid "" "If the user has forgotten his password, you can send him a password reset " "email by clicking this button." @@ -657,27 +747,23 @@ msgstr "" "Si l'utilisateur a oublié son mot de passe, vous pouvez lui envoyer un email " "contenant un lien de réinitilisation en cliquant sur ce bouton." -#: canaille/templates/profile.html:157 -msgid "Submit" -msgstr "Valider" - -#: canaille/templates/profile.html:161 +#: canaille/templates/profile.html:158 msgid "Impersonate" msgstr "Prendre l'identité" -#: canaille/templates/profile.html:167 +#: canaille/templates/profile.html:164 msgid "Delete the user" msgstr "Supprimer l'utilisateur" -#: canaille/templates/profile.html:169 +#: canaille/templates/profile.html:166 msgid "Delete my account" msgstr "Supprimer mon compte" -#: canaille/templates/users.html:17 +#: canaille/templates/users.html:16 msgid "Add a user" msgstr "Nouvel utilisateur" -#: canaille/templates/users.html:23 +#: canaille/templates/users.html:22 msgid "Email" msgstr "Courriel" @@ -697,28 +783,28 @@ msgid "Subject" msgstr "Utilisateur" #: canaille/templates/admin/authorization_list.html:21 -#: canaille/templates/admin/client_list.html:27 +#: canaille/templates/admin/client_list.html:24 #: canaille/templates/admin/token_list.html:21 msgid "Created" msgstr "Créé" -#: canaille/templates/admin/authorization_view.html:8 +#: canaille/templates/admin/authorization_view.html:7 msgid "View a authorization" msgstr "Voir une autorisation" -#: canaille/templates/admin/client_add.html:8 +#: canaille/templates/admin/client_add.html:7 msgid "Add a client" msgstr "Ajouter un client" -#: canaille/templates/admin/client_add.html:14 +#: canaille/templates/admin/client_add.html:11 msgid "Confirm" msgstr "Confirmer" -#: canaille/templates/admin/client_edit.html:13 +#: canaille/templates/admin/client_edit.html:12 msgid "Client deletion" msgstr "Suppression d'un client" -#: canaille/templates/admin/client_edit.html:16 +#: canaille/templates/admin/client_edit.html:15 msgid "" "Are you sure you want to delete this client? This action is unrevokable and " "all the data about this client will be removed." @@ -726,39 +812,39 @@ msgstr "" "Êtes-vous sûrs de vouloir supprimer ce client ? Cette action est irrévocable " "et toutes les données à propos de ce client seront supprimées." -#: canaille/templates/admin/client_edit.html:26 +#: canaille/templates/admin/client_edit.html:25 msgid "Edit a client" msgstr "Éditer un client" -#: canaille/templates/admin/client_edit.html:35 +#: canaille/templates/admin/client_edit.html:32 msgid "ID" msgstr "ID" -#: canaille/templates/admin/client_edit.html:39 +#: canaille/templates/admin/client_edit.html:36 msgid "Secret" msgstr "Secret" -#: canaille/templates/admin/client_edit.html:43 +#: canaille/templates/admin/client_edit.html:40 msgid "Issued at" msgstr "Créé le" -#: canaille/templates/admin/client_edit.html:60 +#: canaille/templates/admin/client_edit.html:57 msgid "Edit" msgstr "Éditer" -#: canaille/templates/admin/client_edit.html:63 +#: canaille/templates/admin/client_edit.html:60 msgid "Delete the client" msgstr "Supprimer le client" -#: canaille/templates/admin/client_list.html:20 +#: canaille/templates/admin/client_list.html:17 msgid "Add client" msgstr "Ajouter un client" -#: canaille/templates/admin/client_list.html:26 +#: canaille/templates/admin/client_list.html:23 msgid "URL" msgstr "URL" -#: canaille/templates/admin/token_view.html:8 +#: canaille/templates/admin/token_view.html:7 msgid "View a token" msgstr "Voir un jeton" diff --git a/canaille/translations/messages.pot b/canaille/translations/messages.pot index 8dc9a3c4..1780db70 100644 --- a/canaille/translations/messages.pot +++ b/canaille/translations/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-06-03 15:21+0200\n" +"POT-Creation-Date: 2021-07-29 16:09+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" -#: canaille/account.py:62 canaille/account.py:87 canaille/oauth.py:58 +#: canaille/account.py:62 canaille/account.py:87 canaille/oauth.py:59 msgid "Login failed, please check your information" msgstr "" @@ -144,9 +144,9 @@ msgstr "" msgid "jdoe" msgstr "" -#: canaille/admin/clients.py:23 canaille/forms.py:82 -#: canaille/templates/admin/client_list.html:25 -#: canaille/templates/users.html:22 +#: canaille/admin/clients.py:23 canaille/forms.py:82 canaille/forms.py:152 +#: canaille/templates/admin/client_list.html:22 +#: canaille/templates/users.html:21 msgid "Name" msgstr "" @@ -170,7 +170,7 @@ msgstr "" msgid "Email address" msgstr "" -#: canaille/forms.py:109 canaille/templates/users.html:24 +#: canaille/forms.py:109 canaille/templates/users.html:23 msgid "Phone number" msgstr "" @@ -190,10 +190,37 @@ msgstr "" msgid "1234" msgstr "" -#: canaille/forms.py:143 +#: canaille/forms.py:143 canaille/templates/base.html:55 +#: canaille/templates/groups.html:9 msgid "Groups" msgstr "" +#: canaille/forms.py:155 +msgid "group" +msgstr "" + +#: canaille/forms.py:162 +msgid "The group '{group}' already exists" +msgstr "" + +#: canaille/groups.py:39 +msgid "Group creation failed." +msgstr "" + +#: canaille/groups.py:46 +#, python-format +msgid "The group %(group)s has been sucessfully created" +msgstr "" + +#: canaille/groups.py:76 +msgid "Group edition failed." +msgstr "" + +#: canaille/groups.py:85 +#, python-format +msgid "The group %(group)s has been sucessfully deleted" +msgstr "" + #: canaille/admin/mail.py:27 canaille/mails.py:27 msgid "Password reset on {website_name}" msgstr "" @@ -218,7 +245,11 @@ msgstr "" msgid "Your phone number." msgstr "" -#: canaille/oauth.py:97 +#: canaille/oauth.py:30 +msgid "Groups you are belonging to" +msgstr "" + +#: canaille/oauth.py:98 msgid "You have been successfully logged out." msgstr "" @@ -298,24 +329,24 @@ msgstr "" msgid "The client has been deleted." msgstr "" -#: canaille/templates/about.html:14 canaille/templates/base.html:90 +#: canaille/templates/about.html:12 canaille/templates/base.html:98 msgid "About canaille" msgstr "" -#: canaille/templates/about.html:16 +#: canaille/templates/about.html:14 msgid "Free and open-source identity provider." msgstr "" -#: canaille/templates/about.html:19 +#: canaille/templates/about.html:17 #, python-format msgid "Version %(version)s" msgstr "" -#: canaille/templates/about.html:20 +#: canaille/templates/about.html:18 msgid "Source code" msgstr "" -#: canaille/templates/about.html:21 +#: canaille/templates/about.html:19 msgid "Documentation" msgstr "" @@ -341,67 +372,67 @@ msgstr "" msgid "Accept" msgstr "" -#: canaille/templates/base.html:8 +#: canaille/templates/base.html:10 msgid "authorization interface" msgstr "" -#: canaille/templates/base.html:37 canaille/templates/profile.html:38 +#: canaille/templates/base.html:39 canaille/templates/profile.html:37 msgid "My profile" msgstr "" -#: canaille/templates/base.html:42 canaille/templates/consent_list.html:19 +#: canaille/templates/base.html:44 canaille/templates/consent_list.html:18 msgid "My consents" msgstr "" -#: canaille/templates/base.html:48 +#: canaille/templates/base.html:50 msgid "Users" msgstr "" -#: canaille/templates/base.html:58 +#: canaille/templates/base.html:65 msgid "Clients" msgstr "" -#: canaille/templates/base.html:62 +#: canaille/templates/base.html:69 msgid "Tokens" msgstr "" -#: canaille/templates/base.html:66 +#: canaille/templates/base.html:73 msgid "Codes" msgstr "" -#: canaille/templates/base.html:70 +#: canaille/templates/base.html:77 msgid "Consents" msgstr "" -#: canaille/templates/base.html:77 +#: canaille/templates/base.html:84 msgid "Log out" msgstr "" -#: canaille/templates/consent_list.html:22 +#: canaille/templates/consent_list.html:21 msgid "Consult and revoke the authorization you gave to websites." msgstr "" -#: canaille/templates/consent_list.html:42 +#: canaille/templates/consent_list.html:39 msgid "From:" msgstr "" -#: canaille/templates/consent_list.html:44 +#: canaille/templates/consent_list.html:41 msgid "Revoked:" msgstr "" -#: canaille/templates/consent_list.html:47 +#: canaille/templates/consent_list.html:44 msgid "Has access to:" msgstr "" -#: canaille/templates/consent_list.html:57 +#: canaille/templates/consent_list.html:54 msgid "Remove access" msgstr "" -#: canaille/templates/consent_list.html:67 +#: canaille/templates/consent_list.html:64 msgid "Nothing here" msgstr "" -#: canaille/templates/consent_list.html:68 +#: canaille/templates/consent_list.html:65 msgid "You did not authorize applications yet." msgstr "" @@ -421,11 +452,11 @@ msgstr "" msgid "Technical problem" msgstr "" -#: canaille/templates/firstlogin.html:12 +#: canaille/templates/firstlogin.html:11 msgid "First login" msgstr "" -#: canaille/templates/firstlogin.html:19 +#: canaille/templates/firstlogin.html:16 msgid "" "\n" " It seems this is the first time you are logging here. In order to" @@ -438,21 +469,21 @@ msgid "" " " msgstr "" -#: canaille/templates/firstlogin.html:35 +#: canaille/templates/firstlogin.html:32 msgid "Send the initialization email" msgstr "" -#: canaille/templates/firstlogin.html:36 -#: canaille/templates/forgotten-password.html:43 +#: canaille/templates/firstlogin.html:33 +#: canaille/templates/forgotten-password.html:40 msgid "Login page" msgstr "" -#: canaille/templates/forgotten-password.html:12 -#: canaille/templates/login.html:38 canaille/templates/password.html:35 +#: canaille/templates/forgotten-password.html:11 +#: canaille/templates/login.html:35 canaille/templates/password.html:32 msgid "Forgotten password" msgstr "" -#: canaille/templates/forgotten-password.html:19 +#: canaille/templates/forgotten-password.html:16 msgid "" "\n" " After this form is sent, if the email address or the login you " @@ -463,181 +494,230 @@ msgid "" " " msgstr "" -#: canaille/templates/forgotten-password.html:38 -#: canaille/templates/profile.html:120 canaille/templates/profile.html:143 +#: canaille/templates/forgotten-password.html:35 +#: canaille/templates/profile.html:117 canaille/templates/profile.html:140 msgid "Send again" msgstr "" -#: canaille/templates/forgotten-password.html:40 +#: canaille/templates/forgotten-password.html:37 msgid "Send" msgstr "" -#: canaille/templates/login.html:17 +#: canaille/templates/group.html:13 +msgid "Group deletion" +msgstr "" + +#: canaille/templates/group.html:17 +msgid "" +"Are you sure you want to delete this group? This action is unrevokable " +"and all the data about this group will be removed." +msgstr "" + +#: canaille/templates/admin/client_edit.html:18 +#: canaille/templates/group.html:21 canaille/templates/profile.html:25 +msgid "Cancel" +msgstr "" + +#: canaille/templates/admin/client_edit.html:19 +#: canaille/templates/group.html:22 canaille/templates/profile.html:26 +msgid "Delete" +msgstr "" + +#: canaille/templates/group.html:29 +msgid "Members" +msgstr "" + +#: canaille/templates/group.html:45 +msgid "Group creation" +msgstr "" + +#: canaille/templates/group.html:47 +msgid "Group edition" +msgstr "" + +#: canaille/templates/group.html:53 +msgid "Create a new group" +msgstr "" + +#: canaille/templates/group.html:55 +msgid "Edit informations about a group" +msgstr "" + +#: canaille/templates/group.html:70 +msgid "" +"Because group cannot be empty, you will be added to the group. You can " +"remove you later by editing your profile when you will have added other " +"members to the group." +msgstr "" + +#: canaille/templates/group.html:75 +msgid "Create group" +msgstr "" + +#: canaille/templates/group.html:77 canaille/templates/profile.html:154 +msgid "Submit" +msgstr "" + +#: canaille/templates/group.html:82 +msgid "Delete group" +msgstr "" + +#: canaille/templates/groups.html:5 +msgid "Add a group" +msgstr "" + +#: canaille/templates/login.html:16 #, python-format msgid "Sign in at %(website)s" msgstr "" -#: canaille/templates/login.html:19 +#: canaille/templates/login.html:18 msgid "Log-in and manage your authorizations." msgstr "" -#: canaille/templates/login.html:37 +#: canaille/templates/login.html:34 msgid "Continue" msgstr "" -#: canaille/templates/password.html:17 +#: canaille/templates/password.html:16 #, python-format msgid "Sign in as %(username)s" msgstr "" -#: canaille/templates/password.html:19 +#: canaille/templates/password.html:18 msgid "Please enter your password for this account." msgstr "" -#: canaille/templates/password.html:34 +#: canaille/templates/password.html:31 msgid "Sign in" msgstr "" -#: canaille/templates/password.html:36 +#: canaille/templates/password.html:33 #, python-format msgid "I am not %(username)s" msgstr "" -#: canaille/templates/profile.html:14 +#: canaille/templates/profile.html:13 msgid "Account deletion" msgstr "" -#: canaille/templates/profile.html:19 +#: canaille/templates/profile.html:18 msgid "" "Are you sure you want to delete this user? This action is unrevokable and" " all the data about this user will be removed." msgstr "" -#: canaille/templates/profile.html:21 +#: canaille/templates/profile.html:20 msgid "" "Are you sure you want to delete your account? This action is unrevokable " "and all your data will be removed forever." msgstr "" -#: canaille/templates/admin/client_edit.html:19 -#: canaille/templates/profile.html:26 -msgid "Cancel" -msgstr "" - -#: canaille/templates/admin/client_edit.html:20 -#: canaille/templates/profile.html:27 -msgid "Delete" -msgstr "" - -#: canaille/templates/profile.html:36 +#: canaille/templates/profile.html:35 msgid "User creation" msgstr "" -#: canaille/templates/profile.html:40 +#: canaille/templates/profile.html:39 msgid "User profile edition" msgstr "" -#: canaille/templates/profile.html:46 +#: canaille/templates/profile.html:45 msgid "Create a new user account" msgstr "" -#: canaille/templates/profile.html:48 +#: canaille/templates/profile.html:47 msgid "Edit your personal informations" msgstr "" -#: canaille/templates/profile.html:50 +#: canaille/templates/profile.html:49 msgid "Edit informations about an user" msgstr "" -#: canaille/templates/profile.html:67 +#: canaille/templates/profile.html:64 msgid "Personal information" msgstr "" -#: canaille/templates/profile.html:89 +#: canaille/templates/profile.html:86 msgid "Account information" msgstr "" -#: canaille/templates/profile.html:105 +#: canaille/templates/profile.html:102 msgid "User password is not mandatory" msgstr "" -#: canaille/templates/profile.html:107 +#: canaille/templates/profile.html:104 msgid "The user password can be set:" msgstr "" -#: canaille/templates/profile.html:109 +#: canaille/templates/profile.html:106 msgid "by filling this form;" msgstr "" -#: canaille/templates/profile.html:110 +#: canaille/templates/profile.html:107 msgid "" "by sending the user a password initialization mail, after the account " "creation;" msgstr "" -#: canaille/templates/profile.html:111 canaille/templates/profile.html:134 +#: canaille/templates/profile.html:108 canaille/templates/profile.html:131 msgid "" "or simply waiting for the user to sign-in a first time, and then receive " "a password initialization mail." msgstr "" -#: canaille/templates/profile.html:124 +#: canaille/templates/profile.html:121 msgid "Send email" msgstr "" -#: canaille/templates/profile.html:128 +#: canaille/templates/profile.html:125 msgid "This user does not have a password yet" msgstr "" -#: canaille/templates/profile.html:130 +#: canaille/templates/profile.html:127 msgid "You can solve this by:" msgstr "" -#: canaille/templates/profile.html:132 +#: canaille/templates/profile.html:129 msgid "setting a password using this form;" msgstr "" -#: canaille/templates/profile.html:133 +#: canaille/templates/profile.html:130 msgid "sending the user a password initialization mail, by clicking this button;" msgstr "" -#: canaille/templates/profile.html:145 +#: canaille/templates/profile.html:142 msgid "Send mail" msgstr "" -#: canaille/templates/profile.html:149 -#: canaille/templates/reset-password.html:12 -#: canaille/templates/reset-password.html:19 +#: canaille/templates/profile.html:146 +#: canaille/templates/reset-password.html:11 +#: canaille/templates/reset-password.html:16 msgid "Password reset" msgstr "" -#: canaille/templates/profile.html:151 +#: canaille/templates/profile.html:148 msgid "" "If the user has forgotten his password, you can send him a password reset" " email by clicking this button." msgstr "" -#: canaille/templates/profile.html:157 -msgid "Submit" -msgstr "" - -#: canaille/templates/profile.html:161 +#: canaille/templates/profile.html:158 msgid "Impersonate" msgstr "" -#: canaille/templates/profile.html:167 +#: canaille/templates/profile.html:164 msgid "Delete the user" msgstr "" -#: canaille/templates/profile.html:169 +#: canaille/templates/profile.html:166 msgid "Delete my account" msgstr "" -#: canaille/templates/users.html:17 +#: canaille/templates/users.html:16 msgid "Add a user" msgstr "" -#: canaille/templates/users.html:23 +#: canaille/templates/users.html:22 msgid "Email" msgstr "" @@ -657,66 +737,66 @@ msgid "Subject" msgstr "" #: canaille/templates/admin/authorization_list.html:21 -#: canaille/templates/admin/client_list.html:27 +#: canaille/templates/admin/client_list.html:24 #: canaille/templates/admin/token_list.html:21 msgid "Created" msgstr "" -#: canaille/templates/admin/authorization_view.html:8 +#: canaille/templates/admin/authorization_view.html:7 msgid "View a authorization" msgstr "" -#: canaille/templates/admin/client_add.html:8 +#: canaille/templates/admin/client_add.html:7 msgid "Add a client" msgstr "" -#: canaille/templates/admin/client_add.html:14 +#: canaille/templates/admin/client_add.html:11 msgid "Confirm" msgstr "" -#: canaille/templates/admin/client_edit.html:13 +#: canaille/templates/admin/client_edit.html:12 msgid "Client deletion" msgstr "" -#: canaille/templates/admin/client_edit.html:16 +#: canaille/templates/admin/client_edit.html:15 msgid "" "Are you sure you want to delete this client? This action is unrevokable " "and all the data about this client will be removed." msgstr "" -#: canaille/templates/admin/client_edit.html:26 +#: canaille/templates/admin/client_edit.html:25 msgid "Edit a client" msgstr "" -#: canaille/templates/admin/client_edit.html:35 +#: canaille/templates/admin/client_edit.html:32 msgid "ID" msgstr "" -#: canaille/templates/admin/client_edit.html:39 +#: canaille/templates/admin/client_edit.html:36 msgid "Secret" msgstr "" -#: canaille/templates/admin/client_edit.html:43 +#: canaille/templates/admin/client_edit.html:40 msgid "Issued at" msgstr "" -#: canaille/templates/admin/client_edit.html:60 +#: canaille/templates/admin/client_edit.html:57 msgid "Edit" msgstr "" -#: canaille/templates/admin/client_edit.html:63 +#: canaille/templates/admin/client_edit.html:60 msgid "Delete the client" msgstr "" -#: canaille/templates/admin/client_list.html:20 +#: canaille/templates/admin/client_list.html:17 msgid "Add client" msgstr "" -#: canaille/templates/admin/client_list.html:26 +#: canaille/templates/admin/client_list.html:23 msgid "URL" msgstr "" -#: canaille/templates/admin/token_view.html:8 +#: canaille/templates/admin/token_view.html:7 msgid "View a token" msgstr "" diff --git a/demo/client/__init__.py b/demo/client/__init__.py index 437ec9d7..b2231164 100644 --- a/demo/client/__init__.py +++ b/demo/client/__init__.py @@ -17,7 +17,7 @@ def create_app(): server_metadata_url=get_well_known_url( app.config["OAUTH_AUTH_SERVER"], external=True ), - client_kwargs={"scope": "openid profile email"}, + client_kwargs={"scope": "openid profile email groups"}, ) @app.route("/") diff --git a/demo/client/templates/index.html b/demo/client/templates/index.html index 0f9455e6..fbc11288 100644 --- a/demo/client/templates/index.html +++ b/demo/client/templates/index.html @@ -53,7 +53,10 @@

      {{ name }}

      {% if user %} - Welcome {{ user.name }} +

      Welcome {{ user.name }}

      + {% if user.groups %} +

      You're a member of the following groups: {{ user.groups }}

      + {% endif %} {% else %} Welcome, please log-in. {% endif %} diff --git a/demo/conf/oauth-authorization-server.json b/demo/conf/oauth-authorization-server.json index 6f75b2ae..49ccc456 100644 --- a/demo/conf/oauth-authorization-server.json +++ b/demo/conf/oauth-authorization-server.json @@ -18,7 +18,7 @@ "http://localhost:5000/oauth/register", "scopes_supported": ["openid", "profile", "email", "address", - "phone"], + "phone", "groups"], "response_types_supported": ["code", "token", "id_token", "code token", "code id_token", "token id_token"], diff --git a/demo/conf/openid-configuration.json b/demo/conf/openid-configuration.json index e6e732a1..060c5db3 100644 --- a/demo/conf/openid-configuration.json +++ b/demo/conf/openid-configuration.json @@ -22,7 +22,7 @@ "http://localhost:5000/oauth/register", "scopes_supported": ["openid", "profile", "email", "address", - "phone"], + "phone", "groups"], "response_types_supported": ["code", "token", "id_token", "code token", "code id_token", "token id_token"], diff --git a/demo/ldif/bootstrap.ldif b/demo/ldif/bootstrap.ldif index ca26a75a..ceaef211 100644 --- a/demo/ldif/bootstrap.ldif +++ b/demo/ldif/bootstrap.ldif @@ -94,6 +94,7 @@ oauthGrantType: refresh_token oauthScope: openid oauthScope: profile oauthScope: email +oauthScope: groups oauthResponseType: code oauthResponseType: id_token oauthTokenEndpointAuthMethod: client_secret_basic @@ -111,6 +112,7 @@ oauthGrantType: refresh_token oauthScope: openid oauthScope: profile oauthScope: email +oauthScope: groups oauthResponseType: code oauthResponseType: id_token oauthTokenEndpointAuthMethod: client_secret_basic diff --git a/tests/conftest.py b/tests/conftest.py index 5fcb2d1c..0232b165 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -215,7 +215,7 @@ def client(app, slapd_connection): "refresh_token", ], oauthResponseType=["code", "token", "id_token"], - oauthScope=["openid", "profile"], + oauthScope=["openid", "profile", "groups"], oauthTermsOfServiceURI="https://mydomain.tld/tos", oauthPolicyURI="https://mydomain.tld/policy", oauthJWKURI="https://mydomain.tld/jwk", @@ -361,7 +361,9 @@ def foo_group(app, user, slapd_connection): g.save(slapd_connection) with app.app_context(): user.load_groups(conn=slapd_connection) - return g + yield g + user._groups = [] + g.delete(conn=slapd_connection) @pytest.fixture @@ -375,9 +377,6 @@ def bar_group(app, admin, slapd_connection): g.save(slapd_connection) with app.app_context(): admin.load_groups(conn=slapd_connection) - return g - - -@pytest.fixture -def groups(foo_group, bar_group, slapd_connection): - return (foo_group, bar_group) + yield g + admin._groups = [] + g.delete(conn=slapd_connection) diff --git a/tests/test_authorization_code_flow.py b/tests/test_authorization_code_flow.py index 8390a6e2..33d62619 100644 --- a/tests/test_authorization_code_flow.py +++ b/tests/test_authorization_code_flow.py @@ -47,7 +47,7 @@ def test_authorization_code_flow(testclient, slapd_connection, logged_user, clie headers={"Authorization": f"Bearer {access_token}"}, status=200, ) - assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json + assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json def test_logout_login(testclient, slapd_connection, logged_user, client): @@ -105,7 +105,7 @@ def test_logout_login(testclient, slapd_connection, logged_user, client): headers={"Authorization": f"Bearer {access_token}"}, status=200, ) - assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json + assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json def test_refresh_token(testclient, slapd_connection, logged_user, client): @@ -164,7 +164,7 @@ def test_refresh_token(testclient, slapd_connection, logged_user, client): headers={"Authorization": f"Bearer {access_token}"}, status=200, ) - assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json + assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json def test_code_challenge(testclient, slapd_connection, logged_user, client): @@ -218,7 +218,7 @@ def test_code_challenge(testclient, slapd_connection, logged_user, client): headers={"Authorization": f"Bearer {access_token}"}, status=200, ) - assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json + assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json client.oauthTokenEndpointAuthMethod = "client_secret_basic" client.save(slapd_connection) diff --git a/tests/test_groups.py b/tests/test_groups.py index 75ca8560..73531b35 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -9,7 +9,7 @@ def test_no_group(app, slapd_connection): def test_set_groups(app, slapd_connection, user, foo_group, bar_group): with app.app_context(): Group.attr_type_by_name(conn=slapd_connection) - a = User.attr_type_by_name(conn=slapd_connection) + User.attr_type_by_name(conn=slapd_connection) user = User.get(dn=user.dn, conn=slapd_connection) assert set(Group.available_groups(conn=slapd_connection)) == { @@ -21,14 +21,80 @@ def test_set_groups(app, slapd_connection, user, foo_group, bar_group): assert user.groups[0].dn == foo_group.dn user.set_groups([foo_group, bar_group], conn=slapd_connection) + bar_dns = {g.dn for g in bar_group.get_members(conn=slapd_connection)} assert user.dn in bar_dns - assert user.groups[1].dn == bar_group.dn user.set_groups([foo_group], conn=slapd_connection) + foo_dns = {g.dn for g in foo_group.get_members(conn=slapd_connection)} bar_dns = {g.dn for g in bar_group.get_members(conn=slapd_connection)} - assert user.dn in foo_dns assert user.dn not in bar_dns + + +def test_moderator_can_create_edit_and_delete_group( + testclient, slapd_connection, logged_moderator, foo_group +): + # The group does not exist + res = testclient.get("/groups", status=200) + with testclient.app.app_context(): + assert Group.get("bar", conn=slapd_connection) is None + assert Group.get("foo", conn=slapd_connection) == foo_group + assert "bar" not in res.text + assert "foo" in res.text + + # Fill the form for a new group + res = testclient.get("/groups/add", status=200) + res.form["name"] = "bar" + + # Group has been created + res = res.form.submit(status=302).follow(status=200) + + with testclient.app.app_context(): + bar_group = Group.get("bar", conn=slapd_connection) + assert bar_group.name == "bar" + assert [ + member.dn for member in bar_group.get_members(conn=slapd_connection) + ] == [ + logged_moderator.dn + ] # Group cannot be empty so creator is added in it + assert "bar" in res.text + + # Group name can not be edited + res = testclient.get("/groups/bar", status=200) + res.form["name"] = "bar2" + + res = res.form.submit(name="action", value="edit", status=200) + + with testclient.app.app_context(): + bar_group = Group.get("bar", conn=slapd_connection) + assert bar_group.name == "bar" + assert Group.get("bar2", conn=slapd_connection) is None + members = bar_group.get_members(conn=slapd_connection) + for member in members: + assert member.name in res.text + + # Group is deleted + res = res.form.submit(name="action", value="delete", status=302).follow(status=200) + with testclient.app.app_context(): + assert Group.get("bar", conn=slapd_connection) is None + assert "The group bar has been sucessfully deleted" in res.text + + +def test_cannot_create_already_existing_group( + testclient, slapd_connection, logged_moderator, foo_group +): + res = testclient.post("/groups/add", {"name": "foo"}, status=200) + + assert "Group creation failed." in res + assert "The group 'foo' already exists" in res + + +def test_simple_user_cannot_view_or_edit_groups( + testclient, slapd_connection, logged_user, foo_group +): + testclient.get("/groups", status=403) + testclient.get("/groups/add", status=403) + testclient.get("/groups/foo", status=403) diff --git a/tests/test_hybrid_flow.py b/tests/test_hybrid_flow.py index 17148337..479a1cef 100644 --- a/tests/test_hybrid_flow.py +++ b/tests/test_hybrid_flow.py @@ -1,9 +1,10 @@ from authlib.jose import jwt from urllib.parse import urlsplit, parse_qs -from canaille.models import AuthorizationCode, Token +from canaille.models import AuthorizationCode, Token, User def test_oauth_hybrid(testclient, slapd_connection, user, client): + User.attr_type_by_name(slapd_connection) res = testclient.get( "/oauth/authorize", params=dict( @@ -41,7 +42,7 @@ def test_oauth_hybrid(testclient, slapd_connection, user, client): headers={"Authorization": f"Bearer {access_token}"}, status=200, ) - assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json + assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json def test_oidc_hybrid(testclient, slapd_connection, logged_user, client, keypair): @@ -80,4 +81,4 @@ def test_oidc_hybrid(testclient, slapd_connection, logged_user, client, keypair) headers={"Authorization": f"Bearer {access_token}"}, status=200, ) - assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json + assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json diff --git a/tests/test_implicit_flow.py b/tests/test_implicit_flow.py index e954a767..9fd2c50e 100644 --- a/tests/test_implicit_flow.py +++ b/tests/test_implicit_flow.py @@ -40,7 +40,7 @@ def test_oauth_implicit(testclient, slapd_connection, user, client): "/oauth/userinfo", headers={"Authorization": f"Bearer {access_token}"} ) assert "application/json" == res.content_type - assert {"name": "John Doe", "sub": "user", "family_name": "Doe"} == res.json + assert {"name": "John Doe", "sub": "user", "family_name": "Doe", "groups": []} == res.json client.oauthGrantType = ["code"] client.oauthTokenEndpointAuthMethod = "client_secret_basic" @@ -92,7 +92,60 @@ def test_oidc_implicit(testclient, keypair, slapd_connection, user, client): status=200, ) assert "application/json" == res.content_type - assert {"name": "John Doe", "sub": "user", "family_name": "Doe"} == res.json + assert {"name": "John Doe", "sub": "user", "family_name": "Doe", "groups": []} == res.json + + client.oauthGrantType = ["code"] + client.oauthTokenEndpointAuthMethod = "client_secret_basic" + client.save(slapd_connection) + + +def test_oidc_implicit_with_group(testclient, keypair, slapd_connection, user, client, foo_group): + client.oauthGrantType = ["token id_token"] + client.oauthTokenEndpointAuthMethod = "none" + + client.save(slapd_connection) + + res = testclient.get( + "/oauth/authorize", + params=dict( + response_type="id_token token", + client_id=client.oauthClientID, + scope="openid profile groups", + nonce="somenonce", + ), + ) + assert "text/html" == res.content_type + + res.form["login"] = "user" + res.form["password"] = "correct horse battery staple" + res = res.form.submit(status=302) + + res = res.follow(status=200) + assert "text/html" == res.content_type, res.json + + res = res.form.submit(name="answer", value="accept", status=302) + + assert res.location.startswith(client.oauthRedirectURIs[0]) + params = parse_qs(urlsplit(res.location).fragment) + + access_token = params["access_token"][0] + token = Token.get(access_token, conn=slapd_connection) + assert token is not None + + id_token = params["id_token"][0] + claims = jwt.decode(id_token, keypair[1]) + assert user.uid[0] == claims["sub"] + assert user.cn[0] == claims["name"] + assert [client.oauthClientID] == claims["aud"] + assert ["foo"] == claims["groups"] + + res = testclient.get( + "/oauth/userinfo", + headers={"Authorization": f"Bearer {access_token}"}, + status=200, + ) + assert "application/json" == res.content_type + assert {"name": "John Doe", "sub": "user", "family_name": "Doe", "groups": ["foo"]} == res.json client.oauthGrantType = ["code"] client.oauthTokenEndpointAuthMethod = "client_secret_basic" diff --git a/tests/test_password_flow.py b/tests/test_password_flow.py index 1a9294bf..07efe2d1 100644 --- a/tests/test_password_flow.py +++ b/tests/test_password_flow.py @@ -15,7 +15,7 @@ def test_password_flow(testclient, slapd_connection, user, client): status=200, ) - assert res.json["scope"] == "openid profile" + assert res.json["scope"] == "openid profile groups" assert res.json["token_type"] == "Bearer" access_token = res.json["access_token"] @@ -27,4 +27,4 @@ def test_password_flow(testclient, slapd_connection, user, client): headers={"Authorization": f"Bearer {access_token}"}, status=200, ) - assert {"name": "John Doe", "sub": "user", "family_name": "Doe"} == res.json + assert {"name": "John Doe", "sub": "user", "family_name": "Doe", "groups": []} == res.json