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/*.pem
canaille/conf/*.pub canaille/conf/*.pub
canaille/conf/*.key canaille/conf/*.key
.vscode

View file

@ -12,6 +12,7 @@ import canaille.consents
import canaille.commands.clean import canaille.commands.clean
import canaille.oauth import canaille.oauth
import canaille.account import canaille.account
import canaille.groups
import canaille.well_known import canaille.well_known
from cryptography.hazmat.primitives import serialization as crypto_serialization from cryptography.hazmat.primitives import serialization as crypto_serialization
@ -136,6 +137,7 @@ def setup_app(app):
config_oauth(app) config_oauth(app)
setup_ldap_tree(app) setup_ldap_tree(app)
app.register_blueprint(canaille.account.bp) 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.oauth.bp, url_prefix="/oauth")
app.register_blueprint(canaille.commands.clean.bp) app.register_blueprint(canaille.commands.clean.bp)
app.register_blueprint(canaille.consents.bp, url_prefix="/consent") app.register_blueprint(canaille.consents.bp, url_prefix="/consent")

View file

@ -18,7 +18,7 @@
"https://mydomain.tld/oauth/register", "https://mydomain.tld/oauth/register",
"scopes_supported": "scopes_supported":
["openid", "profile", "email", "address", ["openid", "profile", "email", "address",
"phone"], "phone", "groups"],
"response_types_supported": "response_types_supported":
["code", "token", "id_token", "code token", ["code", "token", "id_token", "code token",
"code id_token", "token id_token"], "code id_token", "token id_token"],

View file

@ -22,7 +22,7 @@
"https://mydomain.tld/oauth/register", "https://mydomain.tld/oauth/register",
"scopes_supported": "scopes_supported":
["openid", "profile", "email", "address", ["openid", "profile", "email", "address",
"phone"], "phone", "groups"],
"response_types_supported": "response_types_supported":
["code", "token", "id_token", "code token", ["code", "token", "id_token", "code token",
"code id_token", "token id_token"], "code id_token", "token id_token"],

View file

@ -145,3 +145,19 @@ def profile_form(field_names):
render_kw={}, render_kw={},
) )
return wtforms.form.BaseForm(fields) 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) Group.attr_type_by_name(conn=conn)
return [(group[attribute][0], group.dn) for group in groups] 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): def get_members(self, conn=None):
return [User.get(dn=user_dn, conn=conn) for user_dn in self.member] 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.")), "email": ("at", _("Your email address.")),
"address": ("envelope open outline", _("Your postal address.")), "address": ("envelope open outline", _("Your postal address.")),
"phone": ("phone", _("Your phone number.")), "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"] fields += ["address"]
if "phone" in scope: if "phone" in scope:
fields += ["phone_number", "phone_number_verified"] fields += ["phone_number", "phone_number_verified"]
if "groups" in scope:
fields += ["groups"]
data = {} data = {}
for field in fields: for field in fields:
@ -69,6 +71,9 @@ def generate_user_info(user, scope):
data[field] = user.__getattr__(ldap_field_match) data[field] = user.__getattr__(ldap_field_match)
if isinstance(data[field], list): if isinstance(data[field], list):
data[field] = data[field][0] 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) return UserInfo(**data)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %} {% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %} {% block content %}
<div class="loginform"> <div class="loginform">
@ -13,8 +12,6 @@
</div> </div>
</h3> </h3>
{{ flask.messages() }}
<div class="ui attached message"> <div class="ui attached message">
{% trans %} {% trans %}
It seems this is the first time you are logging here. In order to finalize your 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' %} {% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %} {% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %} {% block content %}
<div class="loginform"> <div class="loginform">
@ -13,8 +12,6 @@
</div> </div>
</h3> </h3>
{{ flask.messages() }}
<div class="ui attached message"> <div class="ui attached message">
{% trans %} {% trans %}
After this form is sent, if the email address or the login you provided 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' %} {% extends 'base.html' %}
{% import 'fomanticui.j2' as sui %} {% import 'fomanticui.j2' as sui %}
{% import 'flask.j2' as flask %}
{% block content %} {% block content %}
<div class="ui clearing segment"> <div class="ui clearing segment">
@ -19,8 +18,6 @@
<div class="sub header">{% trans %}Log-in and manage your authorizations.{% endtrans %}</div> <div class="sub header">{% trans %}Log-in and manage your authorizations.{% endtrans %}</div>
</h2> </h2>
{{ flask.messages() }}
<form method="POST" <form method="POST"
id="{{ form.id or form.__class__.__name__|lower }}" id="{{ form.id or form.__class__.__name__|lower }}"
action="{{ request.url }}" action="{{ request.url }}"

View file

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

View file

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

View file

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

View file

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

View file

@ -3,24 +3,25 @@
# This file is distributed under the same license as the PROJECT project. # This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2020. # FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
# Éloi Rivard <eloi.rivard@aquilenet.fr>, 2020-2021. # Éloi Rivard <eloi.rivard@aquilenet.fr>, 2020-2021.
# Camille <camille@yaal.coop>, 2021.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: contact@yaal.fr\n" "Report-Msgid-Bugs-To: contact@yaal.fr\n"
"POT-Creation-Date: 2021-06-03 15:21+0200\n" "POT-Creation-Date: 2021-07-29 16:09+0200\n"
"PO-Revision-Date: 2021-06-03 15:22+0200\n" "PO-Revision-Date: 2021-07-29 16:31+0200\n"
"Last-Translator: Éloi Rivard <eloi.rivard@aquilenet.fr>\n" "Last-Translator: Camille <camille@yaal.coop>\n"
"Language: fr\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" "Plural-Forms: nplurals=2; plural=(n > 1)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\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" msgid "Login failed, please check your information"
msgstr "La connexion a échoué, veuillez vérifier vos informations." msgstr "La connexion a échoué, veuillez vérifier vos informations."
@ -155,9 +156,9 @@ msgstr "Identifiant"
msgid "jdoe" msgid "jdoe"
msgstr "mdupont" msgstr "mdupont"
#: canaille/admin/clients.py:23 canaille/forms.py:82 #: canaille/admin/clients.py:23 canaille/forms.py:82 canaille/forms.py:152
#: canaille/templates/admin/client_list.html:25 #: canaille/templates/admin/client_list.html:22
#: canaille/templates/users.html:22 #: canaille/templates/users.html:21
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
@ -181,7 +182,7 @@ msgstr "Dupont"
msgid "Email address" msgid "Email address"
msgstr "Courriel" msgstr "Courriel"
#: canaille/forms.py:109 canaille/templates/users.html:24 #: canaille/forms.py:109 canaille/templates/users.html:23
msgid "Phone number" msgid "Phone number"
msgstr "Numéro de téléphone" msgstr "Numéro de téléphone"
@ -201,10 +202,37 @@ msgstr "Numéro"
msgid "1234" msgid "1234"
msgstr "1234" msgstr "1234"
#: canaille/forms.py:143 #: canaille/forms.py:143 canaille/templates/base.html:55
#: canaille/templates/groups.html:9
msgid "Groups" msgid "Groups"
msgstr "Groupes" 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 #: canaille/admin/mail.py:27 canaille/mails.py:27
msgid "Password reset on {website_name}" msgid "Password reset on {website_name}"
msgstr "Réinitialisation du mot de passe sur {website_name}" msgstr "Réinitialisation du mot de passe sur {website_name}"
@ -229,7 +257,11 @@ msgstr "Votre adresse postale."
msgid "Your phone number." msgid "Your phone number."
msgstr "Votre numéro de téléphone." 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." msgid "You have been successfully logged out."
msgstr "Vous avez été déconnectés." msgstr "Vous avez été déconnectés."
@ -309,24 +341,24 @@ msgstr "Le client a été édité."
msgid "The client has been deleted." msgid "The client has been deleted."
msgstr "Le client a été supprimé." msgstr "Le client a été supprimé."
#: canaille/templates/about.html:14 canaille/templates/base.html:90 #: canaille/templates/about.html:12 canaille/templates/base.html:98
msgid "About canaille" msgid "About canaille"
msgstr "À propos de canaille" msgstr "À propos de canaille"
#: canaille/templates/about.html:16 #: canaille/templates/about.html:14
msgid "Free and open-source identity provider." msgid "Free and open-source identity provider."
msgstr "Fournisseur d'identité numérique libre" msgstr "Fournisseur d'identité numérique libre"
#: canaille/templates/about.html:19 #: canaille/templates/about.html:17
#, python-format #, python-format
msgid "Version %(version)s" msgid "Version %(version)s"
msgstr "Version %(version)s" msgstr "Version %(version)s"
#: canaille/templates/about.html:20 #: canaille/templates/about.html:18
msgid "Source code" msgid "Source code"
msgstr "Code source" msgstr "Code source"
#: canaille/templates/about.html:21 #: canaille/templates/about.html:19
msgid "Documentation" msgid "Documentation"
msgstr "Documentation" msgstr "Documentation"
@ -352,67 +384,67 @@ msgstr "Changer d'utilisateur"
msgid "Accept" msgid "Accept"
msgstr "Accepter" msgstr "Accepter"
#: canaille/templates/base.html:8 #: canaille/templates/base.html:10
msgid "authorization interface" msgid "authorization interface"
msgstr " - Interface de gestion des autorisations" 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" msgid "My profile"
msgstr "Mon profil" 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" msgid "My consents"
msgstr "Mes autorisations" msgstr "Mes autorisations"
#: canaille/templates/base.html:48 #: canaille/templates/base.html:50
msgid "Users" msgid "Users"
msgstr "Utilisateurs" msgstr "Utilisateurs"
#: canaille/templates/base.html:58 #: canaille/templates/base.html:65
msgid "Clients" msgid "Clients"
msgstr "Clients" msgstr "Clients"
#: canaille/templates/base.html:62 #: canaille/templates/base.html:69
msgid "Tokens" msgid "Tokens"
msgstr "Jetons" msgstr "Jetons"
#: canaille/templates/base.html:66 #: canaille/templates/base.html:73
msgid "Codes" msgid "Codes"
msgstr "Codes" msgstr "Codes"
#: canaille/templates/base.html:70 #: canaille/templates/base.html:77
msgid "Consents" msgid "Consents"
msgstr "Autorisations" msgstr "Autorisations"
#: canaille/templates/base.html:77 #: canaille/templates/base.html:84
msgid "Log out" msgid "Log out"
msgstr "Déconnexion" 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." msgid "Consult and revoke the authorization you gave to websites."
msgstr "Consultez et révoquez les autorisation que vous avez données." 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:" msgid "From:"
msgstr "À partir de :" msgstr "À partir de :"
#: canaille/templates/consent_list.html:44 #: canaille/templates/consent_list.html:41
msgid "Revoked:" msgid "Revoked:"
msgstr "Révoqué le :" msgstr "Révoqué le :"
#: canaille/templates/consent_list.html:47 #: canaille/templates/consent_list.html:44
msgid "Has access to:" msgid "Has access to:"
msgstr "A accès à :" msgstr "A accès à :"
#: canaille/templates/consent_list.html:57 #: canaille/templates/consent_list.html:54
msgid "Remove access" msgid "Remove access"
msgstr "Supprimer l'accès" msgstr "Supprimer l'accès"
#: canaille/templates/consent_list.html:67 #: canaille/templates/consent_list.html:64
msgid "Nothing here" msgid "Nothing here"
msgstr "Rien ici" msgstr "Rien ici"
#: canaille/templates/consent_list.html:68 #: canaille/templates/consent_list.html:65
msgid "You did not authorize applications yet." msgid "You did not authorize applications yet."
msgstr "" msgstr ""
"Vous n'avez pas encore autorisé d'application à accéder à votre profil." "Vous n'avez pas encore autorisé d'application à accéder à votre profil."
@ -433,11 +465,11 @@ msgstr "Page non trouvée"
msgid "Technical problem" msgid "Technical problem"
msgstr "Problème technique" msgstr "Problème technique"
#: canaille/templates/firstlogin.html:12 #: canaille/templates/firstlogin.html:11
msgid "First login" msgid "First login"
msgstr "Première connexion" msgstr "Première connexion"
#: canaille/templates/firstlogin.html:19 #: canaille/templates/firstlogin.html:16
msgid "" msgid ""
"\n" "\n"
" It seems this is the first time you are logging here. In order to " " 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 " " Veuillez cliquer sur le bouton bleu ci-dessous pour envoyer le "
"courriel." "courriel."
#: canaille/templates/firstlogin.html:35 #: canaille/templates/firstlogin.html:32
msgid "Send the initialization email" msgid "Send the initialization email"
msgstr "Envoyer le courriel d'initialisation" msgstr "Envoyer le courriel d'initialisation"
#: canaille/templates/firstlogin.html:36 #: canaille/templates/firstlogin.html:33
#: canaille/templates/forgotten-password.html:43 #: canaille/templates/forgotten-password.html:40
msgid "Login page" msgid "Login page"
msgstr "Page de connexion" msgstr "Page de connexion"
#: canaille/templates/forgotten-password.html:12 #: canaille/templates/forgotten-password.html:11
#: canaille/templates/login.html:38 canaille/templates/password.html:35 #: canaille/templates/login.html:35 canaille/templates/password.html:32
msgid "Forgotten password" msgid "Forgotten password"
msgstr "Mot de passe oublié" msgstr "Mot de passe oublié"
#: canaille/templates/forgotten-password.html:19 #: canaille/templates/forgotten-password.html:16
msgid "" msgid ""
"\n" "\n"
" After this form is sent, if the email address or the login you " " 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" " vous permettra de ré-initialiser votre mot de passe.\n"
" " " "
#: canaille/templates/forgotten-password.html:38 #: canaille/templates/forgotten-password.html:35
#: canaille/templates/profile.html:120 canaille/templates/profile.html:143 #: canaille/templates/profile.html:117 canaille/templates/profile.html:140
msgid "Send again" msgid "Send again"
msgstr "Envoyer à nouveau" msgstr "Envoyer à nouveau"
#: canaille/templates/forgotten-password.html:40 #: canaille/templates/forgotten-password.html:37
msgid "Send" msgid "Send"
msgstr "Envoyer" 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 #, python-format
msgid "Sign in at %(website)s" msgid "Sign in at %(website)s"
msgstr "Connexion à %(website)s" msgstr "Connexion à %(website)s"
#: canaille/templates/login.html:19 #: canaille/templates/login.html:18
msgid "Log-in and manage your authorizations." msgid "Log-in and manage your authorizations."
msgstr "Connectez-vous et gérez vos autorisations." msgstr "Connectez-vous et gérez vos autorisations."
#: canaille/templates/login.html:37 #: canaille/templates/login.html:34
msgid "Continue" msgid "Continue"
msgstr "Continuer" msgstr "Continuer"
#: canaille/templates/password.html:17 #: canaille/templates/password.html:16
#, python-format #, python-format
msgid "Sign in as %(username)s" msgid "Sign in as %(username)s"
msgstr "Connexion en tant que %(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." msgid "Please enter your password for this account."
msgstr "Veuillez entre votre mot de passe pour ce compte." msgstr "Veuillez entre votre mot de passe pour ce compte."
#: canaille/templates/password.html:34 #: canaille/templates/password.html:31
msgid "Sign in" msgid "Sign in"
msgstr "Se connecter" msgstr "Se connecter"
#: canaille/templates/password.html:36 #: canaille/templates/password.html:33
#, python-format #, python-format
msgid "I am not %(username)s" msgid "I am not %(username)s"
msgstr "Je ne suis pas %(username)s" msgstr "Je ne suis pas %(username)s"
#: canaille/templates/profile.html:14 #: canaille/templates/profile.html:13
msgid "Account deletion" msgid "Account deletion"
msgstr "Suppression d'un compte" msgstr "Suppression d'un compte"
#: canaille/templates/profile.html:19 #: canaille/templates/profile.html:18
msgid "" msgid ""
"Are you sure you want to delete this user? This action is unrevokable and " "Are you sure you want to delete this user? This action is unrevokable and "
"all the data about this user will be removed." "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 " "Ê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." "irrévocable, et toutes les données de cet utilisateur seront supprimées."
#: canaille/templates/profile.html:21 #: canaille/templates/profile.html:20
msgid "" msgid ""
"Are you sure you want to delete your account? This action is unrevokable and " "Are you sure you want to delete your account? This action is unrevokable and "
"all your data will be removed forever." "all your data will be removed forever."
@ -550,57 +650,47 @@ msgstr ""
"Êtes-vous sûrs de vouloir supprimer votre compte ? Cette action est " "Êtes-vous sûrs de vouloir supprimer votre compte ? Cette action est "
"irrévocable et toutes vos données seront supprimées pour toujours." "irrévocable et toutes vos données seront supprimées pour toujours."
#: canaille/templates/admin/client_edit.html:19 #: canaille/templates/profile.html:35
#: 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
msgid "User creation" msgid "User creation"
msgstr "Nouvel utilisateur" msgstr "Nouvel utilisateur"
#: canaille/templates/profile.html:40 #: canaille/templates/profile.html:39
msgid "User profile edition" msgid "User profile edition"
msgstr "Édition d'un profil utilisateur" msgstr "Édition d'un profil utilisateur"
#: canaille/templates/profile.html:46 #: canaille/templates/profile.html:45
msgid "Create a new user account" msgid "Create a new user account"
msgstr "Création d'un nouveau compte utilisateur" msgstr "Création d'un nouveau compte utilisateur"
#: canaille/templates/profile.html:48 #: canaille/templates/profile.html:47
msgid "Edit your personal informations" msgid "Edit your personal informations"
msgstr "Éditez vos informations personnelles" msgstr "Éditez vos informations personnelles"
#: canaille/templates/profile.html:50 #: canaille/templates/profile.html:49
msgid "Edit informations about an user" msgid "Edit informations about an user"
msgstr "Éditez les informations d'un utilisateur" msgstr "Éditez les informations d'un utilisateur"
#: canaille/templates/profile.html:67 #: canaille/templates/profile.html:64
msgid "Personal information" msgid "Personal information"
msgstr "Informations personnelles" msgstr "Informations personnelles"
#: canaille/templates/profile.html:89 #: canaille/templates/profile.html:86
msgid "Account information" msgid "Account information"
msgstr "Informations sur le compte" msgstr "Informations sur le compte"
#: canaille/templates/profile.html:105 #: canaille/templates/profile.html:102
msgid "User password is not mandatory" msgid "User password is not mandatory"
msgstr "Le mot de passe utilisateur n'est pas requis à la création" 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:" msgid "The user password can be set:"
msgstr "Il pourra être renseigné :" msgstr "Il pourra être renseigné :"
#: canaille/templates/profile.html:109 #: canaille/templates/profile.html:106
msgid "by filling this form;" msgid "by filling this form;"
msgstr "en remplissant ce formulaire ;" msgstr "en remplissant ce formulaire ;"
#: canaille/templates/profile.html:110 #: canaille/templates/profile.html:107
msgid "" msgid ""
"by sending the user a password initialization mail, after the account " "by sending the user a password initialization mail, after the account "
"creation;" "creation;"
@ -608,7 +698,7 @@ msgstr ""
"en envoyant un lien d'initialisation de mot de passe, par mail à " "en envoyant un lien d'initialisation de mot de passe, par mail à "
"l'utilisateur, après la création de son compte;" "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 "" msgid ""
"or simply waiting for the user to sign-in a first time, and then receive a " "or simply waiting for the user to sign-in a first time, and then receive a "
"password initialization mail." "password initialization mail."
@ -616,40 +706,40 @@ msgstr ""
"ou simplement en attendant la première connexion de l'utilisateur, afin " "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." "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" msgid "Send email"
msgstr "Envoyer l'email" msgstr "Envoyer l'email"
#: canaille/templates/profile.html:128 #: canaille/templates/profile.html:125
msgid "This user does not have a password yet" msgid "This user does not have a password yet"
msgstr "L'utilisateur n'a pas encore de mot de passe" 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:" msgid "You can solve this by:"
msgstr "Vous pouvez régler ceci en :" msgstr "Vous pouvez régler ceci en :"
#: canaille/templates/profile.html:132 #: canaille/templates/profile.html:129
msgid "setting a password using this form;" msgid "setting a password using this form;"
msgstr "renseignant un mot de passe via ce formulaire ;" msgstr "renseignant un mot de passe via ce formulaire ;"
#: canaille/templates/profile.html:133 #: canaille/templates/profile.html:130
msgid "" msgid ""
"sending the user a password initialization mail, by clicking this button;" "sending the user a password initialization mail, by clicking this button;"
msgstr "" msgstr ""
"envoyant un lien d'initialisation de mot de passe, par mail à l'utilisateur, " "envoyant un lien d'initialisation de mot de passe, par mail à l'utilisateur, "
"en cliquant sur ce bouton;" "en cliquant sur ce bouton;"
#: canaille/templates/profile.html:145 #: canaille/templates/profile.html:142
msgid "Send mail" msgid "Send mail"
msgstr "Envoyer l'email" msgstr "Envoyer l'email"
#: canaille/templates/profile.html:149 #: canaille/templates/profile.html:146
#: canaille/templates/reset-password.html:12 #: canaille/templates/reset-password.html:11
#: canaille/templates/reset-password.html:19 #: canaille/templates/reset-password.html:16
msgid "Password reset" msgid "Password reset"
msgstr "Réinitialisation du mot de passe" msgstr "Réinitialisation du mot de passe"
#: canaille/templates/profile.html:151 #: canaille/templates/profile.html:148
msgid "" msgid ""
"If the user has forgotten his password, you can send him a password reset " "If the user has forgotten his password, you can send him a password reset "
"email by clicking this button." "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 " "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." "contenant un lien de réinitilisation en cliquant sur ce bouton."
#: canaille/templates/profile.html:157 #: canaille/templates/profile.html:158
msgid "Submit"
msgstr "Valider"
#: canaille/templates/profile.html:161
msgid "Impersonate" msgid "Impersonate"
msgstr "Prendre l'identité" msgstr "Prendre l'identité"
#: canaille/templates/profile.html:167 #: canaille/templates/profile.html:164
msgid "Delete the user" msgid "Delete the user"
msgstr "Supprimer l'utilisateur" msgstr "Supprimer l'utilisateur"
#: canaille/templates/profile.html:169 #: canaille/templates/profile.html:166
msgid "Delete my account" msgid "Delete my account"
msgstr "Supprimer mon compte" msgstr "Supprimer mon compte"
#: canaille/templates/users.html:17 #: canaille/templates/users.html:16
msgid "Add a user" msgid "Add a user"
msgstr "Nouvel utilisateur" msgstr "Nouvel utilisateur"
#: canaille/templates/users.html:23 #: canaille/templates/users.html:22
msgid "Email" msgid "Email"
msgstr "Courriel" msgstr "Courriel"
@ -697,28 +783,28 @@ msgid "Subject"
msgstr "Utilisateur" msgstr "Utilisateur"
#: canaille/templates/admin/authorization_list.html:21 #: 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 #: canaille/templates/admin/token_list.html:21
msgid "Created" msgid "Created"
msgstr "Créé" msgstr "Créé"
#: canaille/templates/admin/authorization_view.html:8 #: canaille/templates/admin/authorization_view.html:7
msgid "View a authorization" msgid "View a authorization"
msgstr "Voir une autorisation" msgstr "Voir une autorisation"
#: canaille/templates/admin/client_add.html:8 #: canaille/templates/admin/client_add.html:7
msgid "Add a client" msgid "Add a client"
msgstr "Ajouter un client" msgstr "Ajouter un client"
#: canaille/templates/admin/client_add.html:14 #: canaille/templates/admin/client_add.html:11
msgid "Confirm" msgid "Confirm"
msgstr "Confirmer" msgstr "Confirmer"
#: canaille/templates/admin/client_edit.html:13 #: canaille/templates/admin/client_edit.html:12
msgid "Client deletion" msgid "Client deletion"
msgstr "Suppression d'un client" msgstr "Suppression d'un client"
#: canaille/templates/admin/client_edit.html:16 #: canaille/templates/admin/client_edit.html:15
msgid "" msgid ""
"Are you sure you want to delete this client? This action is unrevokable and " "Are you sure you want to delete this client? This action is unrevokable and "
"all the data about this client will be removed." "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 " "Ê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." "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" msgid "Edit a client"
msgstr "Éditer un client" msgstr "Éditer un client"
#: canaille/templates/admin/client_edit.html:35 #: canaille/templates/admin/client_edit.html:32
msgid "ID" msgid "ID"
msgstr "ID" msgstr "ID"
#: canaille/templates/admin/client_edit.html:39 #: canaille/templates/admin/client_edit.html:36
msgid "Secret" msgid "Secret"
msgstr "Secret" msgstr "Secret"
#: canaille/templates/admin/client_edit.html:43 #: canaille/templates/admin/client_edit.html:40
msgid "Issued at" msgid "Issued at"
msgstr "Créé le" msgstr "Créé le"
#: canaille/templates/admin/client_edit.html:60 #: canaille/templates/admin/client_edit.html:57
msgid "Edit" msgid "Edit"
msgstr "Éditer" msgstr "Éditer"
#: canaille/templates/admin/client_edit.html:63 #: canaille/templates/admin/client_edit.html:60
msgid "Delete the client" msgid "Delete the client"
msgstr "Supprimer le client" msgstr "Supprimer le client"
#: canaille/templates/admin/client_list.html:20 #: canaille/templates/admin/client_list.html:17
msgid "Add client" msgid "Add client"
msgstr "Ajouter un client" msgstr "Ajouter un client"
#: canaille/templates/admin/client_list.html:26 #: canaille/templates/admin/client_list.html:23
msgid "URL" msgid "URL"
msgstr "URL" msgstr "URL"
#: canaille/templates/admin/token_view.html:8 #: canaille/templates/admin/token_view.html:7
msgid "View a token" msgid "View a token"
msgstr "Voir un jeton" msgstr "Voir un jeton"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n" "Generated-By: Babel 2.9.1\n"
#: canaille/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" msgid "Login failed, please check your information"
msgstr "" msgstr ""
@ -144,9 +144,9 @@ msgstr ""
msgid "jdoe" msgid "jdoe"
msgstr "" msgstr ""
#: canaille/admin/clients.py:23 canaille/forms.py:82 #: canaille/admin/clients.py:23 canaille/forms.py:82 canaille/forms.py:152
#: canaille/templates/admin/client_list.html:25 #: canaille/templates/admin/client_list.html:22
#: canaille/templates/users.html:22 #: canaille/templates/users.html:21
msgid "Name" msgid "Name"
msgstr "" msgstr ""
@ -170,7 +170,7 @@ msgstr ""
msgid "Email address" msgid "Email address"
msgstr "" msgstr ""
#: canaille/forms.py:109 canaille/templates/users.html:24 #: canaille/forms.py:109 canaille/templates/users.html:23
msgid "Phone number" msgid "Phone number"
msgstr "" msgstr ""
@ -190,10 +190,37 @@ msgstr ""
msgid "1234" msgid "1234"
msgstr "" msgstr ""
#: canaille/forms.py:143 #: canaille/forms.py:143 canaille/templates/base.html:55
#: canaille/templates/groups.html:9
msgid "Groups" msgid "Groups"
msgstr "" 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 #: canaille/admin/mail.py:27 canaille/mails.py:27
msgid "Password reset on {website_name}" msgid "Password reset on {website_name}"
msgstr "" msgstr ""
@ -218,7 +245,11 @@ msgstr ""
msgid "Your phone number." msgid "Your phone number."
msgstr "" 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." msgid "You have been successfully logged out."
msgstr "" msgstr ""
@ -298,24 +329,24 @@ msgstr ""
msgid "The client has been deleted." msgid "The client has been deleted."
msgstr "" msgstr ""
#: canaille/templates/about.html:14 canaille/templates/base.html:90 #: canaille/templates/about.html:12 canaille/templates/base.html:98
msgid "About canaille" msgid "About canaille"
msgstr "" msgstr ""
#: canaille/templates/about.html:16 #: canaille/templates/about.html:14
msgid "Free and open-source identity provider." msgid "Free and open-source identity provider."
msgstr "" msgstr ""
#: canaille/templates/about.html:19 #: canaille/templates/about.html:17
#, python-format #, python-format
msgid "Version %(version)s" msgid "Version %(version)s"
msgstr "" msgstr ""
#: canaille/templates/about.html:20 #: canaille/templates/about.html:18
msgid "Source code" msgid "Source code"
msgstr "" msgstr ""
#: canaille/templates/about.html:21 #: canaille/templates/about.html:19
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
@ -341,67 +372,67 @@ msgstr ""
msgid "Accept" msgid "Accept"
msgstr "" msgstr ""
#: canaille/templates/base.html:8 #: canaille/templates/base.html:10
msgid "authorization interface" msgid "authorization interface"
msgstr "" msgstr ""
#: canaille/templates/base.html:37 canaille/templates/profile.html:38 #: canaille/templates/base.html:39 canaille/templates/profile.html:37
msgid "My profile" msgid "My profile"
msgstr "" 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" msgid "My consents"
msgstr "" msgstr ""
#: canaille/templates/base.html:48 #: canaille/templates/base.html:50
msgid "Users" msgid "Users"
msgstr "" msgstr ""
#: canaille/templates/base.html:58 #: canaille/templates/base.html:65
msgid "Clients" msgid "Clients"
msgstr "" msgstr ""
#: canaille/templates/base.html:62 #: canaille/templates/base.html:69
msgid "Tokens" msgid "Tokens"
msgstr "" msgstr ""
#: canaille/templates/base.html:66 #: canaille/templates/base.html:73
msgid "Codes" msgid "Codes"
msgstr "" msgstr ""
#: canaille/templates/base.html:70 #: canaille/templates/base.html:77
msgid "Consents" msgid "Consents"
msgstr "" msgstr ""
#: canaille/templates/base.html:77 #: canaille/templates/base.html:84
msgid "Log out" msgid "Log out"
msgstr "" msgstr ""
#: canaille/templates/consent_list.html:22 #: canaille/templates/consent_list.html:21
msgid "Consult and revoke the authorization you gave to websites." msgid "Consult and revoke the authorization you gave to websites."
msgstr "" msgstr ""
#: canaille/templates/consent_list.html:42 #: canaille/templates/consent_list.html:39
msgid "From:" msgid "From:"
msgstr "" msgstr ""
#: canaille/templates/consent_list.html:44 #: canaille/templates/consent_list.html:41
msgid "Revoked:" msgid "Revoked:"
msgstr "" msgstr ""
#: canaille/templates/consent_list.html:47 #: canaille/templates/consent_list.html:44
msgid "Has access to:" msgid "Has access to:"
msgstr "" msgstr ""
#: canaille/templates/consent_list.html:57 #: canaille/templates/consent_list.html:54
msgid "Remove access" msgid "Remove access"
msgstr "" msgstr ""
#: canaille/templates/consent_list.html:67 #: canaille/templates/consent_list.html:64
msgid "Nothing here" msgid "Nothing here"
msgstr "" msgstr ""
#: canaille/templates/consent_list.html:68 #: canaille/templates/consent_list.html:65
msgid "You did not authorize applications yet." msgid "You did not authorize applications yet."
msgstr "" msgstr ""
@ -421,11 +452,11 @@ msgstr ""
msgid "Technical problem" msgid "Technical problem"
msgstr "" msgstr ""
#: canaille/templates/firstlogin.html:12 #: canaille/templates/firstlogin.html:11
msgid "First login" msgid "First login"
msgstr "" msgstr ""
#: canaille/templates/firstlogin.html:19 #: canaille/templates/firstlogin.html:16
msgid "" msgid ""
"\n" "\n"
" It seems this is the first time you are logging here. In order to" " It seems this is the first time you are logging here. In order to"
@ -438,21 +469,21 @@ msgid ""
" " " "
msgstr "" msgstr ""
#: canaille/templates/firstlogin.html:35 #: canaille/templates/firstlogin.html:32
msgid "Send the initialization email" msgid "Send the initialization email"
msgstr "" msgstr ""
#: canaille/templates/firstlogin.html:36 #: canaille/templates/firstlogin.html:33
#: canaille/templates/forgotten-password.html:43 #: canaille/templates/forgotten-password.html:40
msgid "Login page" msgid "Login page"
msgstr "" msgstr ""
#: canaille/templates/forgotten-password.html:12 #: canaille/templates/forgotten-password.html:11
#: canaille/templates/login.html:38 canaille/templates/password.html:35 #: canaille/templates/login.html:35 canaille/templates/password.html:32
msgid "Forgotten password" msgid "Forgotten password"
msgstr "" msgstr ""
#: canaille/templates/forgotten-password.html:19 #: canaille/templates/forgotten-password.html:16
msgid "" msgid ""
"\n" "\n"
" After this form is sent, if the email address or the login you " " After this form is sent, if the email address or the login you "
@ -463,181 +494,230 @@ msgid ""
" " " "
msgstr "" msgstr ""
#: canaille/templates/forgotten-password.html:38 #: canaille/templates/forgotten-password.html:35
#: canaille/templates/profile.html:120 canaille/templates/profile.html:143 #: canaille/templates/profile.html:117 canaille/templates/profile.html:140
msgid "Send again" msgid "Send again"
msgstr "" msgstr ""
#: canaille/templates/forgotten-password.html:40 #: canaille/templates/forgotten-password.html:37
msgid "Send" msgid "Send"
msgstr "" 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 #, python-format
msgid "Sign in at %(website)s" msgid "Sign in at %(website)s"
msgstr "" msgstr ""
#: canaille/templates/login.html:19 #: canaille/templates/login.html:18
msgid "Log-in and manage your authorizations." msgid "Log-in and manage your authorizations."
msgstr "" msgstr ""
#: canaille/templates/login.html:37 #: canaille/templates/login.html:34
msgid "Continue" msgid "Continue"
msgstr "" msgstr ""
#: canaille/templates/password.html:17 #: canaille/templates/password.html:16
#, python-format #, python-format
msgid "Sign in as %(username)s" msgid "Sign in as %(username)s"
msgstr "" msgstr ""
#: canaille/templates/password.html:19 #: canaille/templates/password.html:18
msgid "Please enter your password for this account." msgid "Please enter your password for this account."
msgstr "" msgstr ""
#: canaille/templates/password.html:34 #: canaille/templates/password.html:31
msgid "Sign in" msgid "Sign in"
msgstr "" msgstr ""
#: canaille/templates/password.html:36 #: canaille/templates/password.html:33
#, python-format #, python-format
msgid "I am not %(username)s" msgid "I am not %(username)s"
msgstr "" msgstr ""
#: canaille/templates/profile.html:14 #: canaille/templates/profile.html:13
msgid "Account deletion" msgid "Account deletion"
msgstr "" msgstr ""
#: canaille/templates/profile.html:19 #: canaille/templates/profile.html:18
msgid "" msgid ""
"Are you sure you want to delete this user? This action is unrevokable and" "Are you sure you want to delete this user? This action is unrevokable and"
" all the data about this user will be removed." " all the data about this user will be removed."
msgstr "" msgstr ""
#: canaille/templates/profile.html:21 #: canaille/templates/profile.html:20
msgid "" msgid ""
"Are you sure you want to delete your account? This action is unrevokable " "Are you sure you want to delete your account? This action is unrevokable "
"and all your data will be removed forever." "and all your data will be removed forever."
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:19 #: canaille/templates/profile.html:35
#: 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
msgid "User creation" msgid "User creation"
msgstr "" msgstr ""
#: canaille/templates/profile.html:40 #: canaille/templates/profile.html:39
msgid "User profile edition" msgid "User profile edition"
msgstr "" msgstr ""
#: canaille/templates/profile.html:46 #: canaille/templates/profile.html:45
msgid "Create a new user account" msgid "Create a new user account"
msgstr "" msgstr ""
#: canaille/templates/profile.html:48 #: canaille/templates/profile.html:47
msgid "Edit your personal informations" msgid "Edit your personal informations"
msgstr "" msgstr ""
#: canaille/templates/profile.html:50 #: canaille/templates/profile.html:49
msgid "Edit informations about an user" msgid "Edit informations about an user"
msgstr "" msgstr ""
#: canaille/templates/profile.html:67 #: canaille/templates/profile.html:64
msgid "Personal information" msgid "Personal information"
msgstr "" msgstr ""
#: canaille/templates/profile.html:89 #: canaille/templates/profile.html:86
msgid "Account information" msgid "Account information"
msgstr "" msgstr ""
#: canaille/templates/profile.html:105 #: canaille/templates/profile.html:102
msgid "User password is not mandatory" msgid "User password is not mandatory"
msgstr "" msgstr ""
#: canaille/templates/profile.html:107 #: canaille/templates/profile.html:104
msgid "The user password can be set:" msgid "The user password can be set:"
msgstr "" msgstr ""
#: canaille/templates/profile.html:109 #: canaille/templates/profile.html:106
msgid "by filling this form;" msgid "by filling this form;"
msgstr "" msgstr ""
#: canaille/templates/profile.html:110 #: canaille/templates/profile.html:107
msgid "" msgid ""
"by sending the user a password initialization mail, after the account " "by sending the user a password initialization mail, after the account "
"creation;" "creation;"
msgstr "" msgstr ""
#: canaille/templates/profile.html:111 canaille/templates/profile.html:134 #: canaille/templates/profile.html:108 canaille/templates/profile.html:131
msgid "" msgid ""
"or simply waiting for the user to sign-in a first time, and then receive " "or simply waiting for the user to sign-in a first time, and then receive "
"a password initialization mail." "a password initialization mail."
msgstr "" msgstr ""
#: canaille/templates/profile.html:124 #: canaille/templates/profile.html:121
msgid "Send email" msgid "Send email"
msgstr "" msgstr ""
#: canaille/templates/profile.html:128 #: canaille/templates/profile.html:125
msgid "This user does not have a password yet" msgid "This user does not have a password yet"
msgstr "" msgstr ""
#: canaille/templates/profile.html:130 #: canaille/templates/profile.html:127
msgid "You can solve this by:" msgid "You can solve this by:"
msgstr "" msgstr ""
#: canaille/templates/profile.html:132 #: canaille/templates/profile.html:129
msgid "setting a password using this form;" msgid "setting a password using this form;"
msgstr "" msgstr ""
#: canaille/templates/profile.html:133 #: canaille/templates/profile.html:130
msgid "sending the user a password initialization mail, by clicking this button;" msgid "sending the user a password initialization mail, by clicking this button;"
msgstr "" msgstr ""
#: canaille/templates/profile.html:145 #: canaille/templates/profile.html:142
msgid "Send mail" msgid "Send mail"
msgstr "" msgstr ""
#: canaille/templates/profile.html:149 #: canaille/templates/profile.html:146
#: canaille/templates/reset-password.html:12 #: canaille/templates/reset-password.html:11
#: canaille/templates/reset-password.html:19 #: canaille/templates/reset-password.html:16
msgid "Password reset" msgid "Password reset"
msgstr "" msgstr ""
#: canaille/templates/profile.html:151 #: canaille/templates/profile.html:148
msgid "" msgid ""
"If the user has forgotten his password, you can send him a password reset" "If the user has forgotten his password, you can send him a password reset"
" email by clicking this button." " email by clicking this button."
msgstr "" msgstr ""
#: canaille/templates/profile.html:157 #: canaille/templates/profile.html:158
msgid "Submit"
msgstr ""
#: canaille/templates/profile.html:161
msgid "Impersonate" msgid "Impersonate"
msgstr "" msgstr ""
#: canaille/templates/profile.html:167 #: canaille/templates/profile.html:164
msgid "Delete the user" msgid "Delete the user"
msgstr "" msgstr ""
#: canaille/templates/profile.html:169 #: canaille/templates/profile.html:166
msgid "Delete my account" msgid "Delete my account"
msgstr "" msgstr ""
#: canaille/templates/users.html:17 #: canaille/templates/users.html:16
msgid "Add a user" msgid "Add a user"
msgstr "" msgstr ""
#: canaille/templates/users.html:23 #: canaille/templates/users.html:22
msgid "Email" msgid "Email"
msgstr "" msgstr ""
@ -657,66 +737,66 @@ msgid "Subject"
msgstr "" msgstr ""
#: canaille/templates/admin/authorization_list.html:21 #: 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 #: canaille/templates/admin/token_list.html:21
msgid "Created" msgid "Created"
msgstr "" msgstr ""
#: canaille/templates/admin/authorization_view.html:8 #: canaille/templates/admin/authorization_view.html:7
msgid "View a authorization" msgid "View a authorization"
msgstr "" msgstr ""
#: canaille/templates/admin/client_add.html:8 #: canaille/templates/admin/client_add.html:7
msgid "Add a client" msgid "Add a client"
msgstr "" msgstr ""
#: canaille/templates/admin/client_add.html:14 #: canaille/templates/admin/client_add.html:11
msgid "Confirm" msgid "Confirm"
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:13 #: canaille/templates/admin/client_edit.html:12
msgid "Client deletion" msgid "Client deletion"
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:16 #: canaille/templates/admin/client_edit.html:15
msgid "" msgid ""
"Are you sure you want to delete this client? This action is unrevokable " "Are you sure you want to delete this client? This action is unrevokable "
"and all the data about this client will be removed." "and all the data about this client will be removed."
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:26 #: canaille/templates/admin/client_edit.html:25
msgid "Edit a client" msgid "Edit a client"
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:35 #: canaille/templates/admin/client_edit.html:32
msgid "ID" msgid "ID"
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:39 #: canaille/templates/admin/client_edit.html:36
msgid "Secret" msgid "Secret"
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:43 #: canaille/templates/admin/client_edit.html:40
msgid "Issued at" msgid "Issued at"
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:60 #: canaille/templates/admin/client_edit.html:57
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#: canaille/templates/admin/client_edit.html:63 #: canaille/templates/admin/client_edit.html:60
msgid "Delete the client" msgid "Delete the client"
msgstr "" msgstr ""
#: canaille/templates/admin/client_list.html:20 #: canaille/templates/admin/client_list.html:17
msgid "Add client" msgid "Add client"
msgstr "" msgstr ""
#: canaille/templates/admin/client_list.html:26 #: canaille/templates/admin/client_list.html:23
msgid "URL" msgid "URL"
msgstr "" msgstr ""
#: canaille/templates/admin/token_view.html:8 #: canaille/templates/admin/token_view.html:7
msgid "View a token" msgid "View a token"
msgstr "" msgstr ""

View file

@ -17,7 +17,7 @@ def create_app():
server_metadata_url=get_well_known_url( server_metadata_url=get_well_known_url(
app.config["OAUTH_AUTH_SERVER"], external=True app.config["OAUTH_AUTH_SERVER"], external=True
), ),
client_kwargs={"scope": "openid profile email"}, client_kwargs={"scope": "openid profile email groups"},
) )
@app.route("/") @app.route("/")

View file

@ -53,7 +53,10 @@
<h2 class="ui header">{{ name }}</h2> <h2 class="ui header">{{ name }}</h2>
<div> <div>
{% if user %} {% 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 %} {% else %}
Welcome, please <a href="{{ url_for('login') }}">log-in</a>. Welcome, please <a href="{{ url_for('login') }}">log-in</a>.
{% endif %} {% endif %}

View file

@ -18,7 +18,7 @@
"http://localhost:5000/oauth/register", "http://localhost:5000/oauth/register",
"scopes_supported": "scopes_supported":
["openid", "profile", "email", "address", ["openid", "profile", "email", "address",
"phone"], "phone", "groups"],
"response_types_supported": "response_types_supported":
["code", "token", "id_token", "code token", ["code", "token", "id_token", "code token",
"code id_token", "token id_token"], "code id_token", "token id_token"],

View file

@ -22,7 +22,7 @@
"http://localhost:5000/oauth/register", "http://localhost:5000/oauth/register",
"scopes_supported": "scopes_supported":
["openid", "profile", "email", "address", ["openid", "profile", "email", "address",
"phone"], "phone", "groups"],
"response_types_supported": "response_types_supported":
["code", "token", "id_token", "code token", ["code", "token", "id_token", "code token",
"code id_token", "token id_token"], "code id_token", "token id_token"],

View file

@ -94,6 +94,7 @@ oauthGrantType: refresh_token
oauthScope: openid oauthScope: openid
oauthScope: profile oauthScope: profile
oauthScope: email oauthScope: email
oauthScope: groups
oauthResponseType: code oauthResponseType: code
oauthResponseType: id_token oauthResponseType: id_token
oauthTokenEndpointAuthMethod: client_secret_basic oauthTokenEndpointAuthMethod: client_secret_basic
@ -111,6 +112,7 @@ oauthGrantType: refresh_token
oauthScope: openid oauthScope: openid
oauthScope: profile oauthScope: profile
oauthScope: email oauthScope: email
oauthScope: groups
oauthResponseType: code oauthResponseType: code
oauthResponseType: id_token oauthResponseType: id_token
oauthTokenEndpointAuthMethod: client_secret_basic oauthTokenEndpointAuthMethod: client_secret_basic

View file

@ -215,7 +215,7 @@ def client(app, slapd_connection):
"refresh_token", "refresh_token",
], ],
oauthResponseType=["code", "token", "id_token"], oauthResponseType=["code", "token", "id_token"],
oauthScope=["openid", "profile"], oauthScope=["openid", "profile", "groups"],
oauthTermsOfServiceURI="https://mydomain.tld/tos", oauthTermsOfServiceURI="https://mydomain.tld/tos",
oauthPolicyURI="https://mydomain.tld/policy", oauthPolicyURI="https://mydomain.tld/policy",
oauthJWKURI="https://mydomain.tld/jwk", oauthJWKURI="https://mydomain.tld/jwk",
@ -361,7 +361,9 @@ def foo_group(app, user, slapd_connection):
g.save(slapd_connection) g.save(slapd_connection)
with app.app_context(): with app.app_context():
user.load_groups(conn=slapd_connection) user.load_groups(conn=slapd_connection)
return g yield g
user._groups = []
g.delete(conn=slapd_connection)
@pytest.fixture @pytest.fixture
@ -375,9 +377,6 @@ def bar_group(app, admin, slapd_connection):
g.save(slapd_connection) g.save(slapd_connection)
with app.app_context(): with app.app_context():
admin.load_groups(conn=slapd_connection) admin.load_groups(conn=slapd_connection)
return g yield g
admin._groups = []
g.delete(conn=slapd_connection)
@pytest.fixture
def groups(foo_group, bar_group, slapd_connection):
return (foo_group, bar_group)

View file

@ -47,7 +47,7 @@ def test_authorization_code_flow(testclient, slapd_connection, logged_user, clie
headers={"Authorization": f"Bearer {access_token}"}, headers={"Authorization": f"Bearer {access_token}"},
status=200, 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): 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}"}, headers={"Authorization": f"Bearer {access_token}"},
status=200, 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): 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}"}, headers={"Authorization": f"Bearer {access_token}"},
status=200, 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): 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}"}, headers={"Authorization": f"Bearer {access_token}"},
status=200, 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.oauthTokenEndpointAuthMethod = "client_secret_basic"
client.save(slapd_connection) 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): def test_set_groups(app, slapd_connection, user, foo_group, bar_group):
with app.app_context(): with app.app_context():
Group.attr_type_by_name(conn=slapd_connection) 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) user = User.get(dn=user.dn, conn=slapd_connection)
assert set(Group.available_groups(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 assert user.groups[0].dn == foo_group.dn
user.set_groups([foo_group, bar_group], conn=slapd_connection) user.set_groups([foo_group, bar_group], conn=slapd_connection)
bar_dns = {g.dn for g in bar_group.get_members(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.dn in bar_dns
assert user.groups[1].dn == bar_group.dn assert user.groups[1].dn == bar_group.dn
user.set_groups([foo_group], conn=slapd_connection) user.set_groups([foo_group], conn=slapd_connection)
foo_dns = {g.dn for g in foo_group.get_members(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)} bar_dns = {g.dn for g in bar_group.get_members(conn=slapd_connection)}
assert user.dn in foo_dns assert user.dn in foo_dns
assert user.dn not in bar_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 authlib.jose import jwt
from urllib.parse import urlsplit, parse_qs 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): def test_oauth_hybrid(testclient, slapd_connection, user, client):
User.attr_type_by_name(slapd_connection)
res = testclient.get( res = testclient.get(
"/oauth/authorize", "/oauth/authorize",
params=dict( params=dict(
@ -41,7 +42,7 @@ def test_oauth_hybrid(testclient, slapd_connection, user, client):
headers={"Authorization": f"Bearer {access_token}"}, headers={"Authorization": f"Bearer {access_token}"},
status=200, 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): 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}"}, headers={"Authorization": f"Bearer {access_token}"},
status=200, 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}"} "/oauth/userinfo", headers={"Authorization": f"Bearer {access_token}"}
) )
assert "application/json" == res.content_type 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.oauthGrantType = ["code"]
client.oauthTokenEndpointAuthMethod = "client_secret_basic" client.oauthTokenEndpointAuthMethod = "client_secret_basic"
@ -92,7 +92,60 @@ def test_oidc_implicit(testclient, keypair, slapd_connection, user, client):
status=200, status=200,
) )
assert "application/json" == res.content_type 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.oauthGrantType = ["code"]
client.oauthTokenEndpointAuthMethod = "client_secret_basic" client.oauthTokenEndpointAuthMethod = "client_secret_basic"

View file

@ -15,7 +15,7 @@ def test_password_flow(testclient, slapd_connection, user, client):
status=200, status=200,
) )
assert res.json["scope"] == "openid profile" assert res.json["scope"] == "openid profile groups"
assert res.json["token_type"] == "Bearer" assert res.json["token_type"] == "Bearer"
access_token = res.json["access_token"] 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}"}, headers={"Authorization": f"Bearer {access_token}"},
status=200, 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