From 977b457d5ce7661e8b4680d5543ad4d31063dcf1 Mon Sep 17 00:00:00 2001 From: Alejandro Sirgo Rica Date: Tue, 25 Jun 2024 17:29:02 +0200 Subject: ogcp: add user permission mechanism Add a new user permission system to control the allowed operations accessible from each account. Add a permission matrix editable through the user/add and user/edit views. The permission matrix has client, center, room, folder, image and repository as permission targets and add, update and delete as permission types. Restrict each view based on the user permissions, hide all actions from not autheticated users. permissions defined in the class UserForm. Serialize each user permissions into ogcp.json as: { ... "USERS" [ { "USER": "admin" ... "PERMISSIONS": { "CLIENT": { "ADD": true, "UPDATE": true, "DELETE": true, }, ... <- same structure for "CENTER", "ROOM", "FOLDER", "IMAGE" and "REPOSITORY" } }, ... ], ... } Grant all the permissions to old user configuration to not disrupt their workflow. The administrator will need to assign the permissions for each user. Ignore scope and permission restrictions for admin users. Save permissions and scopes even if the user is admin to account for the case of a temporal admin promotion without losing the previous configuration. Use template inheritance for add_user.html and edit_user.html to prevent big code duplication with the new HTML code to render the permission matrix. Make user administration an admin only feature. Define methods get_permission and target_is_disabled to improve readability in template conditionals that disable features based on user permissions. --- ogcp/forms/auth.py | 15 ++++- ogcp/models.py | 13 +++- ogcp/templates/auth/add_user.html | 54 +--------------- ogcp/templates/auth/edit_user.html | 54 ++-------------- ogcp/templates/auth/user_form.html | 126 +++++++++++++++++++++++++++++++++++++ ogcp/templates/base.html | 2 +- ogcp/templates/commands.html | 8 ++- ogcp/templates/images.html | 16 ++--- ogcp/templates/repos.html | 24 ++++--- ogcp/templates/scopes.html | 57 +++++++++++++---- ogcp/templates/servers.html | 2 + ogcp/templates/users.html | 2 + ogcp/views.py | 72 ++++++++++++++++++++- 13 files changed, 309 insertions(+), 136 deletions(-) create mode 100644 ogcp/templates/auth/user_form.html diff --git a/ogcp/forms/auth.py b/ogcp/forms/auth.py index d85931b..a76ec7c 100644 --- a/ogcp/forms/auth.py +++ b/ogcp/forms/auth.py @@ -7,7 +7,8 @@ from wtforms import ( Form, SubmitField, HiddenField, SelectField, BooleanField, IntegerField, - StringField, RadioField, PasswordField, SelectMultipleField, widgets + StringField, RadioField, PasswordField, SelectMultipleField, FormField, + widgets ) from wtforms.validators import InputRequired, Optional from flask_wtf import FlaskForm @@ -28,6 +29,12 @@ class LoginForm(FlaskForm): ) +class PermissionForm(FlaskForm): + add = BooleanField(_l('Add'), default=True) + update = BooleanField(_l('Update'), default=True) + delete = BooleanField(_l('Delete'), default=True) + + class UserForm(FlaskForm): username = StringField( label=_l('Username'), @@ -50,6 +57,12 @@ class UserForm(FlaskForm): option_widget=widgets.CheckboxInput(), widget=widgets.ListWidget(prefix_label=False) ) + client_permissions = FormField(PermissionForm, label=_l('Client Permissions')) + center_permissions = FormField(PermissionForm, label=_l('Center Permissions')) + room_permissions = FormField(PermissionForm, label=_l('Room Permissions')) + folder_permissions = FormField(PermissionForm, label=_l('Folder Permissions')) + image_permissions = FormField(PermissionForm, label=_l('Image Permissions')) + repository_permissions = FormField(PermissionForm, label=_l('Repository Permissions')) submit_btn = SubmitField( label=_l('Submit') ) diff --git a/ogcp/models.py b/ogcp/models.py index d27b869..ef050ed 100644 --- a/ogcp/models.py +++ b/ogcp/models.py @@ -8,7 +8,18 @@ from flask_login import UserMixin class User(UserMixin): - def __init__(self, username, scopes, admin): + def __init__(self, username, scopes, admin, permissions): self.id = username self.scopes = scopes self.admin = admin + self.permissions = permissions + + def get_permission(self, target, action): + if self.admin or not target in self.permissions: + return True + return self.permissions[target].get(action, True) + + def target_is_disabled(self, target): + if self.admin or not target in self.permissions or not self.permissions[target]: + return False + return all(value == False for value in self.permissions[target].values()) diff --git a/ogcp/templates/auth/add_user.html b/ogcp/templates/auth/add_user.html index cc5ed09..4661236 100644 --- a/ogcp/templates/auth/add_user.html +++ b/ogcp/templates/auth/add_user.html @@ -1,53 +1,5 @@ -{% extends 'users.html' %} -{% import "bootstrap/wtf.html" as wtf %} +{% extends 'auth/user_form.html' %} -{% set sidebar_state = 'disabled' %} -{% set btn_back = true %} +{% block subhead_heading %}{{_('Add user')}}{% endblock %} -{% block nav_user_add %}active{% endblock %} -{% block content %} - -

{{_('Add a user')}}

- -
- {{ form.hidden_tag() }} - -
- {{ form.username.label(class_='form-label') }} - {{ form.username(class_='form-control') }} -
- -
- {{ form.pwd.label(class_='form-label') }} - {{ form.pwd(class_='form-control') }} -
- -
- {{ form.pwd_confirm.label(class_='form-label') }} - {{ form.pwd_confirm(class_='form-control') }} -
- -
- {{ form.admin(class_='form-check-input') }} - {{ form.admin.label(class_='form-check-label') }} -
- -
- {{ form.scopes.label(class_='form-label') }} -
{{ form.scopes.description }}
-
- {% for value, label, checked in form.scopes.iter_choices() %} -
- - -
- {% endfor %} -
-
- -
- {{ form.submit_btn(class_='btn btn-primary') }} -
-
- -{% endblock %} +{% block form_action %}{{ url_for('user_add_post') }}{% endblock %} diff --git a/ogcp/templates/auth/edit_user.html b/ogcp/templates/auth/edit_user.html index 3b10508..42ba5aa 100644 --- a/ogcp/templates/auth/edit_user.html +++ b/ogcp/templates/auth/edit_user.html @@ -1,53 +1,9 @@ -{% extends 'users.html' %} -{% import "bootstrap/wtf.html" as wtf %} +{% extends 'auth/user_form.html' %} -{% set sidebar_state = 'disabled' %} -{% set btn_back = true %} +{% block subhead_heading %}{{_('Edit user {}').format(form.username.data)}}{% endblock %} -{% block nav_user_edit %}active{% endblock %} -{% block content %} +{% block form_action %}{{ url_for('user_edit_post') }}{% endblock %} -

{{_('Edit user {}').format(form.username.data)}}

+{% block pwd_field %}{% endblock %} -
- {{ form.hidden_tag() }} - -
- {{ form.username.label(class_='form-label') }} - {{ form.username(class_='form-control') }} -
- -
- {{ form.pwd.label(class_='form-label') }} - -
- -
- {{ form.pwd_confirm.label(class_='form-label') }} - -
- -
- {{ form.admin(class_='form-check-input') }} - {{ form.admin.label(class_='form-check-label') }} -
- -
- {{ form.scopes.label(class_='form-label') }} -
{{ form.scopes.description }}
-
- {% for value, label, checked in form.scopes.iter_choices() %} -
- - -
- {% endfor %} -
-
- -
- {{ form.submit_btn(class_='btn btn-primary') }} -
-
- -{% endblock %} +{% block pwd_confirm_field %}{% endblock %} diff --git a/ogcp/templates/auth/user_form.html b/ogcp/templates/auth/user_form.html new file mode 100644 index 0000000..7b6b338 --- /dev/null +++ b/ogcp/templates/auth/user_form.html @@ -0,0 +1,126 @@ +{% extends 'users.html' %} +{% import "bootstrap/wtf.html" as wtf %} + +{% set sidebar_state = 'disabled' %} +{% set btn_back = true %} + +{% block nav_user_add %}active{% endblock %} +{% block content %} + +

{% block subhead_heading %}{% endblock %}

+ +
+ {{ form.hidden_tag() }} + +
+ {{ form.username.label(class_='form-label') }} + {{ form.username(class_='form-control') }} +
+ +
+ {{ form.pwd.label(class_='form-label') }} + {% block pwd_field %}{{ form.pwd(class_='form-control') }}{% endblock %} +
+ +
+ {{ form.pwd_confirm.label(class_='form-label') }} + {% block pwd_confirm_field %}{{ form.pwd_confirm(class_='form-control') }}{% endblock %} +
+ +
+
+ {{ form.admin(class_="custom-control-input", id="adminToggle") }} + +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ form.client_permissions.add.label.text }}{{ form.client_permissions.update.label.text }}{{ form.client_permissions.delete.label.text }}
{{ form.client_permissions.label }}{{ form.client_permissions.add() }}{{ form.client_permissions.update() }}{{ form.client_permissions.delete() }}
{{ form.center_permissions.label }}{{ form.center_permissions.add() }}{{ form.center_permissions.update() }}{{ form.center_permissions.delete() }}
{{ form.room_permissions.label }}{{ form.room_permissions.add() }}{{ form.room_permissions.update() }}{{ form.room_permissions.delete() }}
{{ form.folder_permissions.label }}{{ form.folder_permissions.add() }}{{ form.folder_permissions.update() }}{{ form.folder_permissions.delete() }}
{{ form.image_permissions.label }}{{ form.image_permissions.add() }}{{ form.image_permissions.update() }}{{ form.image_permissions.delete() }}
{{ form.repository_permissions.label }}{{ form.repository_permissions.add() }}{{ form.repository_permissions.update() }}{{ form.repository_permissions.delete() }}
+
+ +
+ {{ form.scopes.label(class_='form-label') }} +
{{ form.scopes.description }}
+
+ {% for value, label, checked in form.scopes.iter_choices() %} +
+ + +
+ {% endfor %} +
+
+
+ +
+ {{ form.submit_btn(class_='btn btn-primary') }} +
+
+ +{% endblock %} diff --git a/ogcp/templates/base.html b/ogcp/templates/base.html index 9839029..2af9873 100644 --- a/ogcp/templates/base.html +++ b/ogcp/templates/base.html @@ -36,10 +36,10 @@ - {% if current_user.admin %} + {% if current_user.admin %} diff --git a/ogcp/templates/commands.html b/ogcp/templates/commands.html index 7a63c38..9bb0176 100644 --- a/ogcp/templates/commands.html +++ b/ogcp/templates/commands.html @@ -16,7 +16,7 @@ {% endblock %} {% block commands %} - +{% if current_user.is_authenticated %} - +{% endif %} {% if btn_back %} {% endif %} - {% if current_user.admin %} + + + {% if not current_user.target_is_disabled('FOLDER') %} - {% endif %} +{% endif %} + {% if btn_back %}