Add support for roles

Requires patches to prosody trunk which have been submitted
already (2021-03-22) which introduce the set_roles function on
usermanager.

Fixes #42.
This commit is contained in:
Jonas Schäfer
2021-03-22 21:29:30 +01:00
parent cca899bd8c
commit ea7ed7c030
8 changed files with 193 additions and 52 deletions

View File

@@ -65,6 +65,15 @@ class EditUserForm(BaseForm):
_l("Display name"),
)
role = wtforms.RadioField(
_l("Access Level"),
choices=[
("prosody:restricted", _l("Limited")),
("prosody:normal", _l("Normal user")),
("prosody:admin", _l("Administrator")),
],
)
action_save = wtforms.SubmitField(
_l("Update user"),
)
@@ -99,6 +108,7 @@ async def edit_user(localpart: str) -> typing.Union[quart.Response, str]:
await client.update_user(
localpart,
display_name=form.display_name.data,
roles=[form.role.data],
)
await flash(
@@ -110,6 +120,10 @@ async def edit_user(localpart: str) -> typing.Union[quart.Response, str]:
elif request.method == "GET":
form.localpart.data = target_user_info.localpart
form.display_name.data = target_user_info.display_name
if target_user_info.roles:
form.role.data = target_user_info.roles[0]
else:
form.role.data = "prosody:normal"
return await render_template(
"admin_edit_user.html",

View File

@@ -44,6 +44,15 @@ class AdminUserInfo:
display_name: typing.Optional[str]
email: typing.Optional[str]
phone: typing.Optional[str]
roles: typing.Optional[typing.List[str]]
@property
def has_admin_role(self) -> bool:
return bool(self.roles and "prosody:admin" in self.roles)
@property
def has_restricted_role(self) -> bool:
return bool(self.roles and "prosody:restricted" in self.roles)
@classmethod
def from_api_response(
@@ -55,6 +64,7 @@ class AdminUserInfo:
display_name=data.get("display_name") or None,
email=data.get("email") or None,
phone=data.get("phone") or None,
roles=data.get("roles"),
)
@@ -864,13 +874,16 @@ class ProsodyClient:
localpart: str,
*,
display_name: typing.Optional[str],
roles: typing.Optional[typing.Collection[str]],
session: aiohttp.ClientSession,
) -> None:
payload = {
payload: typing.Dict[str, typing.Any] = {
"username": localpart,
}
if display_name is not None:
payload["display_name"] = display_name
if roles is not None:
payload["roles"] = list(roles)
async with session.put(
self._admin_v1_endpoint("/users/{}".format(localpart)),

View File

@@ -354,6 +354,15 @@ div.form.layout-expanded {
display: block;
}
.radio-button-ext > label > p {
margin-left: 1.75rem;
margin-top: 0;
}
.radio-button-ext > label .icon {
margin-left: 0.25em;
}
div.select-wrap {
display: block;
border-bottom: $w-s4 solid $primary-500;

View File

@@ -37,6 +37,11 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M10.79 16.29c.39.39 1.02.39 1.41 0l3.59-3.59c.39-.39.39-1.02 0-1.41L12.2 7.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L12.67 11H4c-.55 0-1 .45-1 1s.45 1 1 1h8.67l-1.88 1.88c-.39.39-.38 1.03 0 1.41zM19 3H5c-1.11 0-2 .9-2 2v3c0 .55.45 1 1 1s1-.45 1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1v3c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
</symbol>
<!-- from: action/lock/materialiconsround/24px.svg -->
<symbol id="icon-lock" viewBox="0 0 24 24">
<g fill="none"><path d="M0 0h24v24H0V0z" /><path d="M0 0h24v24H0V0z" opacity=".87" /></g>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z" />
</symbol>
<!-- from: communication/qr_code/materialiconsround/24px.svg -->
<symbol id="icon-qrcode" viewBox="0 0 24 24">
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,5 +1,21 @@
{% extends "admin_app.html" %}
{% from "library.j2" import box, form_button, standard_button %}
{% from "library.j2" import box, form_button, standard_button, icon %}
{% macro access_level_description(role, caller=None) %}
{%- if role == "prosody:restricted" -%}
{% trans %}Limited users can interact with users on the same Snikket service and be members of circles.{% endtrans %}
{%- elif role == "prosody:normal" -%}
{% trans %}Like limited users and can also interact with users on other Snikket services.{% endtrans %}
{%- elif role == "prosody:admin" -%}
{% trans %}Like normal users and can access the admin panel in the web portal.{% endtrans %}
{%- endif -%}
{% endmacro %}
{% macro access_level_icon(role, caller=None) %}
{%- if role == "prosody:restricted" -%}
{% call icon("lock") %}{% endcall %}
{%- elif role == "prosody:admin" -%}
{% call icon("admin") %}{% endcall %}
{%- endif -%}
{% endmacro %}
{% block content %}
<h1>{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST">
@@ -14,6 +30,22 @@
{{ form.display_name.label }}
{{ form.display_name }}
</div>
<h3 class="form-title">{% trans %}Access Level{% endtrans %}</h3>
<p class="form-descr weak">{% trans %}The access level of a user determines what interactions are allowed for them on your Snikket service.{% endtrans %}</p>
<div class="f-ebox">
<fieldset>{#- -#}
<legend class="a11y-only">{{ form.role.label.text }}</legend>
{%- for level in form.role -%}
<div class="radio-button-ext">
{{ level }}<label for="{{ level.id }}">
{%- trans title=level.label.text, icon=access_level_icon(level.data), description=access_level_description(level.data) -%}
<strong>{{ title }}{{ icon }}</strong><p>{{ description }}</p>
{%- endtrans -%}
</label>
</div>
{%- endfor -%}
</fieldset>
</div>
<div class="f-bbox">
{%- call standard_button("back", url_for(".users"), class="tertiary") -%}
{%- trans -%}Return to user list{%- endtrans -%}

View File

@@ -1,5 +1,5 @@
{% extends "admin_app.html" %}
{% from "library.j2" import action_button, value_or_hint, custom_form_button %}
{% from "library.j2" import action_button, icon, value_or_hint, custom_form_button %}
{% block content %}
<h1>{% trans %}Manage users{% endtrans %}</h1>
<div class="elevated el-2"><table>
@@ -13,7 +13,15 @@
<tbody>
{% for user in users %}
<tr>
<td>{{ user.localpart }}</td>
<td>
{{- user.localpart -}}
{%- if user.has_admin_role -%}
<span class="with-tooltip above" data-tooltip="{% trans %}The user is an administrator.{% endtrans %}">{% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %}</span>
{%- endif -%}
{%- if user.has_restricted_role -%}
<span class="with-tooltip above" data-tooltip="{% trans %}The user is restricted.{% endtrans %}">{% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %}</span>
{%- endif -%}
</td>
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
<td class="nowrap">
{%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%}

View File

@@ -27,147 +27,163 @@ msgstr ""
msgid "Display name"
msgstr ""
#: snikket_web/admin.py:69
msgid "Update user"
#: snikket_web/admin.py:69 snikket_web/templates/admin_edit_user.html:33
msgid "Access Level"
msgstr ""
#: snikket_web/admin.py:71
msgid "Limited"
msgstr ""
#: snikket_web/admin.py:72
msgid "Normal user"
msgstr ""
#: snikket_web/admin.py:73
msgid "Administrator"
msgstr ""
#: snikket_web/admin.py:78
msgid "Update user"
msgstr ""
#: snikket_web/admin.py:82
msgid "Create password reset link"
msgstr ""
#: snikket_web/admin.py:91
#: snikket_web/admin.py:100
msgid "Password reset link created"
msgstr ""
#: snikket_web/admin.py:105
#: snikket_web/admin.py:115
msgid "User information updated."
msgstr ""
#: snikket_web/admin.py:123
#: snikket_web/admin.py:137
msgid "Delete user permanently"
msgstr ""
#: snikket_web/admin.py:136
#: snikket_web/admin.py:150
msgid "User deleted"
msgstr ""
#: snikket_web/admin.py:174
#: snikket_web/admin.py:188
msgid "Password reset link not found"
msgstr ""
#: snikket_web/admin.py:186
#: snikket_web/admin.py:200
msgid "Password reset link deleted"
msgstr ""
#: snikket_web/admin.py:206
#: snikket_web/admin.py:220
msgid "Invite to circle"
msgstr ""
#: snikket_web/admin.py:212
#: snikket_web/admin.py:226
msgid "At least one circle must be selected"
msgstr ""
#: snikket_web/admin.py:217
#: snikket_web/admin.py:231
msgid "Valid for"
msgstr ""
#: snikket_web/admin.py:219
#: snikket_web/admin.py:233
msgid "One hour"
msgstr ""
#: snikket_web/admin.py:220
#: snikket_web/admin.py:234
msgid "Twelve hours"
msgstr ""
#: snikket_web/admin.py:221
#: snikket_web/admin.py:235
msgid "One day"
msgstr ""
#: snikket_web/admin.py:222
#: snikket_web/admin.py:236
msgid "One week"
msgstr ""
#: snikket_web/admin.py:223
#: snikket_web/admin.py:237
msgid "Four weeks"
msgstr ""
#: snikket_web/admin.py:229 snikket_web/templates/admin_edit_invite.html:17
#: snikket_web/admin.py:243 snikket_web/templates/admin_edit_invite.html:17
msgid "Invitation type"
msgstr ""
#: snikket_web/admin.py:231 snikket_web/templates/library.j2:116
#: snikket_web/admin.py:245 snikket_web/templates/library.j2:116
msgid "Individual"
msgstr ""
#: snikket_web/admin.py:232 snikket_web/templates/library.j2:114
#: snikket_web/admin.py:246 snikket_web/templates/library.j2:114
msgid "Group"
msgstr ""
#: snikket_web/admin.py:238
#: snikket_web/admin.py:252
msgid "New invitation link"
msgstr ""
#: snikket_web/admin.py:300
#: snikket_web/admin.py:314
msgid "Revoke"
msgstr ""
#: snikket_web/admin.py:324
#: snikket_web/admin.py:338
msgid "Invitation created"
msgstr ""
#: snikket_web/admin.py:340
#: snikket_web/admin.py:354
msgid "No such invitation exists"
msgstr ""
#: snikket_web/admin.py:355
#: snikket_web/admin.py:369
msgid "Invitation revoked"
msgstr ""
#: snikket_web/admin.py:372 snikket_web/admin.py:420
#: snikket_web/admin.py:386 snikket_web/admin.py:434
msgid "Name"
msgstr ""
#: snikket_web/admin.py:377 snikket_web/templates/admin_circles.html:47
#: snikket_web/admin.py:391 snikket_web/templates/admin_circles.html:47
msgid "Create circle"
msgstr ""
#: snikket_web/admin.py:407
#: snikket_web/admin.py:421
msgid "Circle created"
msgstr ""
#: snikket_web/admin.py:425
#: snikket_web/admin.py:439
msgid "Select user"
msgstr ""
#: snikket_web/admin.py:430
#: snikket_web/admin.py:444
msgid "Update circle"
msgstr ""
#: snikket_web/admin.py:434
#: snikket_web/admin.py:448
msgid "Delete circle permanently"
msgstr ""
#: snikket_web/admin.py:440
#: snikket_web/admin.py:454
msgid "Add user"
msgstr ""
#: snikket_web/admin.py:456
#: snikket_web/admin.py:470
msgid "No such circle exists"
msgstr ""
#: snikket_web/admin.py:493
#: snikket_web/admin.py:507
msgid "Circle data updated"
msgstr ""
#: snikket_web/admin.py:499
#: snikket_web/admin.py:513
msgid "Circle deleted"
msgstr ""
#: snikket_web/admin.py:510
#: snikket_web/admin.py:524
msgid "User added to circle"
msgstr ""
#: snikket_web/admin.py:519
#: snikket_web/admin.py:533
msgid "User removed from circle"
msgstr ""
@@ -481,7 +497,7 @@ msgid "Delete user %(user_name)s"
msgstr ""
#: snikket_web/templates/admin_delete_user.html:6
#: snikket_web/templates/admin_edit_user.html:22
#: snikket_web/templates/admin_edit_user.html:54
msgid "Delete user"
msgstr ""
@@ -626,51 +642,78 @@ msgstr ""
msgid "Return to invitation list"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:4
#: snikket_web/templates/admin_users.html:20
#: snikket_web/templates/admin_edit_user.html:5
msgid ""
"Limited users can interact with users on the same Snikket service and be "
"members of circles."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:7
msgid ""
"Like limited users and can also interact with users on other Snikket "
"services."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:9
msgid "Like normal users and can access the admin panel in the web portal."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:20
#: snikket_web/templates/admin_users.html:28
#, python-format
msgid "Edit user %(user_name)s"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:7
#: snikket_web/templates/admin_edit_user.html:23
msgid "Edit user"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:11
#: snikket_web/templates/admin_edit_user.html:27
msgid "The login name cannot be changed."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:19
#: snikket_web/templates/admin_edit_user.html:34
msgid ""
"The access level of a user determines what interactions are allowed for "
"them on your Snikket service."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:41
#, python-format
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:51
msgid "Return to user list"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:27
#: snikket_web/templates/admin_edit_user.html:59
msgid "Further actions"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:29
#: snikket_web/templates/admin_edit_user.html:61
msgid "Reset password"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:32
#: snikket_web/templates/admin_edit_user.html:64
msgid ""
"If the user has lost their password, you can use the button below to "
"create a special link which allows to change the password of the account,"
" once."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:37
#: snikket_web/templates/admin_edit_user.html:69
msgid "Debug information"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:39
#: snikket_web/templates/admin_edit_user.html:71
msgid ""
"In some cases, extended information about the user account and the "
"connected devices is necessary to troubleshoot issues. The button below "
"reveals this (sensitive) information."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:43
#: snikket_web/templates/admin_edit_user.html:75
msgid "Show debug information"
msgstr ""
@@ -768,6 +811,22 @@ msgstr ""
msgid "Destroy link"
msgstr ""
#: snikket_web/templates/admin_users.html:19
msgid "The user is an administrator."
msgstr ""
#: snikket_web/templates/admin_users.html:19
msgid " (Administrator)"
msgstr ""
#: snikket_web/templates/admin_users.html:22
msgid "The user is restricted."
msgstr ""
#: snikket_web/templates/admin_users.html:22
msgid " (Restricted)"
msgstr ""
#: snikket_web/templates/app.html:4
msgid "Snikket Web Portal"
msgstr ""

View File

@@ -5,6 +5,7 @@ action/delete:delete
action/logout:logout
action/login:login
action/exit_to_app:exit_to_app
action/lock:lock
communication/qr_code:qrcode
communication/vpn_key:passwd
content/add_circle_outline:add