Merge branch 'issue-12-groups' into 'master'

Create, edit and delete groups from interface (moderators only)

See merge request yaal/canaille!6
This commit is contained in:
Camille 2021-07-29 14:43:51 +00:00
commit f4d6e723ba
41 changed files with 768 additions and 284 deletions

1
.gitignore vendored
View file

@ -19,3 +19,4 @@ canaille/conf/openid-configuration.json
canaille/conf/*.pem
canaille/conf/*.pub
canaille/conf/*.key
.vscode

View file

@ -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")

View file

@ -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"],

View file

@ -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"],

View file

@ -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)
)

89
canaille/groups.py Normal file
View file

@ -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("/<groupname>", 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"))

View file

@ -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]

View file

@ -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")),
}

View file

@ -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)

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="ui clearing segment">
@ -8,7 +7,6 @@
<img class="ui tiny centered image" src="/static/img/canaille-head.png" alt="{{ website_name }}">
</a>
{{ flask.messages() }}
<h2 class="ui center aligned header">
<div class="content">
{{ _("About canaille") }}

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="loginform">
@ -8,8 +7,6 @@
{% trans %}View a authorization{% endtrans %}
</h3>
{{ flask.messages() }}
<div class="ui attached clearing segment">
<ul>
{% for attr in authorization.may %}

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="loginform">
@ -8,8 +7,6 @@
{% trans %}Add a client{% endtrans %}
</h3>
{{ flask.messages() }}
<div class="ui attached clearing segment">
{{ sui.render_form(form, _("Confirm")) }}
</div>

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block script %}
<script src="/static/js/admin/client_edit.js"></script>
@ -26,8 +25,6 @@
{% trans %}Edit a client{% endtrans %}
</h3>
{{ flask.messages() }}
<div class="ui attached clearing segment">
<div class="ui form">
<form id="readonly">

View file

@ -1,5 +1,4 @@
{% extends 'base.html' %}
{% import 'flask.j2' as flask %}
{% block style %}
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
@ -14,8 +13,6 @@
{% block content %}
{{ flask.messages() }}
<div class="ui segment">
<a class="ui primary button" href="{{ url_for('admin_clients.add') }}">{% trans %}Add client{% endtrans %}</a>
</div>

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="loginform">
@ -8,8 +7,6 @@
{% trans %}View a token{% endtrans %}
</h3>
{{ flask.messages() }}
<div class="ui attached clearing segment">
<ul>
{% for attr in token.may %}

View file

@ -1,3 +1,5 @@
{% import 'flask.j2' as flask %}
<!doctype html>
<html lang="fr">
<head>
@ -47,6 +49,11 @@
<i class="users icon"></i>
{% trans %}Users{% endtrans %}
</a>
<a class="item {% if menuitem == "groups" %}active{% endif %}"
href="{{ url_for('groups.groups') }}">
<i class="users cog icon"></i>
{% trans %}Groups{% endtrans %}
</a>
{% endif %}
{% if user.admin %}
<div class="ui dropdown item {% if menuitem == "admin" %}active{% endif %}">
@ -82,6 +89,7 @@
<div class="ui container">
<div class="content">
{{ flask.messages() }}
{% block content %}{% endblock %}
</div>
</div>

View file

@ -1,5 +1,4 @@
{% extends 'base.html' %}
{% import 'flask.j2' as flask %}
{% block style %}
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
@ -23,8 +22,6 @@
</div>
</h2>
{{ flask.messages() }}
{% if consents %}
<div class="ui centered cards">
{% for consent in consents %}

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="loginform">
@ -13,8 +12,6 @@
</div>
</h3>
{{ flask.messages() }}
<div class="ui attached message">
{% trans %}
It seems this is the first time you are logging here. In order to finalize your

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="loginform">
@ -13,8 +12,6 @@
</div>
</h3>
{{ flask.messages() }}
<div class="ui attached message">
{% trans %}
After this form is sent, if the email address or the login you provided

View file

@ -0,0 +1,87 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% block script %}
<script src="/static/js/confirm.js"></script>
{% endblock %}
{% block content %}
{% if edited_group %}
<div class="ui basic modal">
<div class="ui icon header">
<i class="trash alternate icon"></i>
{% trans %}Group deletion{% endtrans %}
</div>
<div class="content">
<p>
{% trans %}Are you sure you want to delete this group? This action is unrevokable and all the data about this group will be removed.{% endtrans %}
</p>
</div>
<div class="actions">
<div class="ui inverted cancel button">{% trans %}Cancel{% endtrans %}</div>
<div class="ui inverted red approve button">{% trans %}Delete{% endtrans %}</div>
</div>
</div>
{% endif %}
{% if edited_group %}
<div class="ui segment">
<h2 class="ui header">{% trans %}Members{% endtrans %}</h2>
<ul>
{% for member in members %}
<div class="ui left icon">
<i class="user icon"></i>
<a href="{{ url_for('account.profile_edition', username=member.uid[0]) }}">{{ member.name }}</a></li>
</div>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="ui clearing segment">
<h2 class="ui center aligned header">
<div class="content">
{% if not edited_group %}
{% trans %}Group creation{% endtrans %}
{% else %}
{% trans %}Group edition{% endtrans %}
{% endif %}
</div>
<div class="sub header">
{% if not edited_group %}
{% trans %}Create a new group{% endtrans %}
{% else %}
{% trans %}Edit informations about a group{% endtrans %}
{% endif %}
</div>
</h2>
<form method="POST"
id="{{ form.__class__.__name__|lower }}"
action="{{ request.url }}"
role="form"
class="ui form"
>
{{ form.hidden_tag() if form.hidden_tag }}
{{ sui.render_field(form.name) }}
{% if not edited_group %}
<p>
{% 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 %}
</p>
{% endif %}
<button type="submit" class="ui right floated primary button" name="action" value="edit">
{% if not edited_group %}
{% trans %}Create group{% endtrans %}
{% else %}
{% trans %}Submit{% endtrans %}
{% endif %}
</button>
{% if edited_group %}
<button type="submit" class="ui right floated basic negative button confirm" name="action" value="delete" id="delete">
{% trans %}Delete group{% endtrans %}
</button>
{% endif %}
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block content %}
<div class="ui segment">
<a class="ui primary button" href="{{ url_for('groups.create_group') }}">{% trans %}Add a group{% endtrans %}</a>
</div>
<div class="ui segment">
<h2 class="ui header">{% trans %}Groups{% endtrans %}</h2>
<ul>
{% for group in groups %}
<div class="ui left icon">
<i class="users icon"></i>
<a href="{{ url_for('groups.group', groupname=group.name) }}">{{ group.name }}</a>
</div>
{% endfor %}
</ul>
</div>
{% endblock %}

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="ui clearing segment">
@ -19,8 +18,6 @@
<div class="sub header">{% trans %}Log-in and manage your authorizations.{% endtrans %}</div>
</h2>
{{ flask.messages() }}
<form method="POST"
id="{{ form.id or form.__class__.__name__|lower }}"
action="{{ request.url }}"

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="ui clearing segment">
@ -19,8 +18,6 @@
<div class="sub header">{% trans %}Please enter your password for this account.{% endtrans %}</div>
</h2>
{{ flask.messages() }}
<form method="POST"
id="{{ form.id or form.__class__.__name__|lower }}"
action="{{ request.url }}"

View file

@ -1,9 +1,8 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block script %}
<script src="/static/js/profile.js"></script>
<script src="/static/js/confirm.js"></script>
{% endblock %}
{% block content %}
@ -52,8 +51,6 @@
</div>
</h2>
{{ flask.messages() }}
<form method="POST"
id="{{ form.__class__.__name__|lower }}"
action="{{ request.url }}"

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %}
<div class="loginform">
@ -13,8 +12,6 @@
</div>
</h3>
{{ flask.messages() }}
<div class="ui attached clearing segment">
{{ sui.render_form(form, _("Password reset"), action=url_for("account.reset", uid=uid, hash=hash)) }}
</div>

View file

@ -12,7 +12,6 @@
{% endblock %}
{% block content %}
<div class="ui segment">
<a class="ui primary button" href="{{ url_for('account.profile_creation') }}">{% trans %}Add a user{% endtrans %}</a>
</div>

View file

@ -3,24 +3,25 @@
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
# Éloi Rivard <eloi.rivard@aquilenet.fr>, 2020-2021.
# Camille <camille@yaal.coop>, 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 <eloi.rivard@aquilenet.fr>\n"
"POT-Creation-Date: 2021-07-29 16:09+0200\n"
"PO-Revision-Date: 2021-07-29 16:31+0200\n"
"Last-Translator: Camille <camille@yaal.coop>\n"
"Language: fr\n"
"Language-Team: French <traduc@traduc.org>\n"
"Language-Team: French <contact@yaal.coop>\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"

View file

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

View file

@ -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("/")

View file

@ -53,7 +53,10 @@
<h2 class="ui header">{{ name }}</h2>
<div>
{% if user %}
Welcome {{ user.name }}
<p>Welcome {{ user.name }}</p>
{% if user.groups %}
<p>You're a member of the following groups: {{ user.groups }}</p>
{% endif %}
{% else %}
Welcome, please <a href="{{ url_for('login') }}">log-in</a>.
{% endif %}

View file

@ -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"],

View file

@ -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"],

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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