From 3598c71c50522f63541d9a2ad1151f96558db24d Mon Sep 17 00:00:00 2001 From: Sam Arbid Date: Mon, 3 Nov 2025 18:27:04 +0100 Subject: [PATCH 1/6] feat(roles): add administration views for role management * Introduce RolesListView, RolesDetailView, RolesCreateView, and RolesEditView for managing user roles. * Update setup configuration to include new role views. --- .../administration/roles/__init__.py | 13 +++ invenio_app_rdm/administration/roles/roles.py | 105 ++++++++++++++++++ setup.cfg | 5 + 3 files changed, 123 insertions(+) create mode 100644 invenio_app_rdm/administration/roles/__init__.py create mode 100644 invenio_app_rdm/administration/roles/roles.py diff --git a/invenio_app_rdm/administration/roles/__init__.py b/invenio_app_rdm/administration/roles/__init__.py new file mode 100644 index 000000000..3a6019994 --- /dev/null +++ b/invenio_app_rdm/administration/roles/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2025 CERN. +# Copyright (C) 2025 KTH Royal Institute of Technology. +# +# Invenio App RDM is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Invenio administration module for groups / roles resources.""" + +from .roles import RolesCreateView, RolesDetailView, RolesEditView, RolesListView + +__all__ = ("RolesListView", "RolesDetailView", "RolesCreateView", "RolesEditView") diff --git a/invenio_app_rdm/administration/roles/roles.py b/invenio_app_rdm/administration/roles/roles.py new file mode 100644 index 000000000..6b3990c36 --- /dev/null +++ b/invenio_app_rdm/administration/roles/roles.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2025 CERN. +# Copyright (C) 2025 KTH Royal Institute of Technology. +# +# Invenio App RDM is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Administration views for managing roles.""" + +from invenio_administration.views.base import ( + AdminResourceCreateView, + AdminResourceDetailView, + AdminResourceEditView, + AdminResourceListView, +) +from invenio_i18n import lazy_gettext as _ + + +class RoleAdminMixin: + """Shared configuration for role administration views.""" + + resource_config = "groups_resource" + extension_name = "invenio-users-resources" + + api_endpoint = "/groups" + pid_path = "name" + + create_view_name = "roles_create" + list_view_name = "roles" + + display_search = True + display_delete = True + display_create = True + display_edit = True + + search_config_name = "USERS_RESOURCES_GROUPS_ADMIN_SEARCH" + search_sort_config_name = "USERS_RESOURCES_GROUPS_ADMIN_SORT_OPTIONS" + search_facets_config_name = "USERS_RESOURCES_GROUPS_ADMIN_FACETS" + + +class RolesListView(RoleAdminMixin, AdminResourceListView): + """List roles.""" + + name = "roles" + title = _("Roles") + menu_label = _("Roles") + category = _("User management") + icon = "id badge" + order = 30 + + item_field_list = { + "name": {"text": _("Name"), "order": 1, "width": 3}, + "description": {"text": _("Description"), "order": 2, "width": 5}, + "is_managed": {"text": _("Managed"), "order": 3, "width": 1}, + "created": {"text": _("Created"), "order": 4, "width": 2}, + } + + search_request_headers = {"Accept": "application/json"} + + +class RolesDetailView(RoleAdminMixin, AdminResourceDetailView): + """Role detail view.""" + + url = "/roles/" + name = "roles_detail" + title = _("Role details") + + item_field_list = { + "name": {"text": _("Name"), "order": 1}, + "description": {"text": _("Description"), "order": 2}, + "is_managed": {"text": _("Managed"), "order": 3}, + "created": {"text": _("Created"), "order": 4}, + "updated": {"text": _("Updated"), "order": 5}, + } + + +class RolesCreateView(RoleAdminMixin, AdminResourceCreateView): + """Role creation view.""" + + name = "roles_create" + url = "/roles/create" + title = _("Create role") + + form_fields = { + "name": { + "order": 1, + "text": _("Name"), + "required": True, + }, + "description": { + "order": 2, + "text": _("Description"), + }, + } + + +class RolesEditView(RoleAdminMixin, AdminResourceEditView): + """Role edit view.""" + + name = "roles_edit" + url = "/roles//edit" + title = _("Edit role") + + form_fields = RolesCreateView.form_fields diff --git a/setup.cfg b/setup.cfg index 84745bd96..f16aabd76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,7 @@ # Copyright (C) 2019-2022 Northwestern University. # Copyright (C) 2022-2025 Graz University of Technology. # Copyright (C) 2025 University of Münster. +# Copyright (C) 2025 KTH Royal Institute of Technology. # # Invenio App RDM is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -119,6 +120,10 @@ invenio_administration.views = invenio_requests_user_moderation_detail = invenio_app_rdm.administration.user_moderation:UserModerationRequestDetailView invenio_requests_moderation_requests_list = invenio_app_rdm.administration.moderation.requests:ModerationRequestListView invenio_requests_moderation_requests_details = invenio_app_rdm.administration.moderation.requests:ModerationRequestDetailView + invenio_users_resources_roles_list = invenio_app_rdm.administration.roles:RolesListView + invenio_users_resources_roles_edit = invenio_app_rdm.administration.roles:RolesEditView + invenio_users_resources_roles_detail = invenio_app_rdm.administration.roles:RolesDetailView + invenio_users_resources_roles_create = invenio_app_rdm.administration.roles:RolesCreateView invenio_app_rdm_records_list = invenio_app_rdm.administration.records:RecordAdminListView invenio_app_rdm_drafts_list = invenio_app_rdm.administration.records:DraftAdminListView invenio_app_rdm_audit_logs = invenio_app_rdm.administration.audit_logs:AuditLogListView From 523ecdb93cfb302f3fc6208b7853fb9406ac8ed5 Mon Sep 17 00:00:00 2001 From: Sam Arbid Date: Tue, 4 Nov 2025 01:15:26 +0100 Subject: [PATCH 2/6] feat: add roles to user detail view page --- invenio_app_rdm/administration/users/users.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/invenio_app_rdm/administration/users/users.py b/invenio_app_rdm/administration/users/users.py index 07f4b8afe..d2888aec0 100644 --- a/invenio_app_rdm/administration/users/users.py +++ b/invenio_app_rdm/administration/users/users.py @@ -36,11 +36,12 @@ "status": {"text": _("Status"), "order": 6, "width": 1}, "visibility": {"text": _("Visibility"), "order": 7, "width": 1}, "active": {"text": _("Active"), "order": 8, "width": 1}, - "confirmed_at": {"text": _("Confirmed at"), "order": 9, "width": 1}, - "verified_at": {"text": _("Verified at"), "order": 10, "width": 1}, - "blocked_at": {"text": _("Blocked at"), "order": 11, "width": 1}, - "created": {"text": _("Created"), "order": 12, "width": 2}, - "updated": {"text": _("Updated"), "order": 13, "width": 2}, + "roles": {"text": _("Roles"), "order": 9, "width": 2}, + "confirmed_at": {"text": _("Confirmed at"), "order": 10, "width": 1}, + "verified_at": {"text": _("Verified at"), "order": 11, "width": 1}, + "blocked_at": {"text": _("Blocked at"), "order": 12, "width": 1}, + "created": {"text": _("Created"), "order": 13, "width": 2}, + "updated": {"text": _("Updated"), "order": 14, "width": 2}, } From e1e7b318bf027416cab17b6707049db01af5f5a4 Mon Sep 17 00:00:00 2001 From: Sam Arbid Date: Thu, 13 Nov 2025 14:36:50 +0100 Subject: [PATCH 3/6] feat: add ID field to roles list and detail views * The latest change included an ID field. * The role name and ID are now separate fields. --- invenio_app_rdm/administration/roles/roles.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invenio_app_rdm/administration/roles/roles.py b/invenio_app_rdm/administration/roles/roles.py index 6b3990c36..824856059 100644 --- a/invenio_app_rdm/administration/roles/roles.py +++ b/invenio_app_rdm/administration/roles/roles.py @@ -50,6 +50,7 @@ class RolesListView(RoleAdminMixin, AdminResourceListView): order = 30 item_field_list = { + "id": {"text": _("ID"), "order": 0, "width": 1}, "name": {"text": _("Name"), "order": 1, "width": 3}, "description": {"text": _("Description"), "order": 2, "width": 5}, "is_managed": {"text": _("Managed"), "order": 3, "width": 1}, @@ -67,6 +68,7 @@ class RolesDetailView(RoleAdminMixin, AdminResourceDetailView): title = _("Role details") item_field_list = { + "id": {"text": _("ID"), "order": 0}, "name": {"text": _("Name"), "order": 1}, "description": {"text": _("Description"), "order": 2}, "is_managed": {"text": _("Managed"), "order": 3}, From a2b88b4b111b550bd58e4afeca598d4b99f84ee4 Mon Sep 17 00:00:00 2001 From: Sam Arbid Date: Thu, 13 Nov 2025 15:06:47 +0100 Subject: [PATCH 4/6] fix: update pid_path to use ID instead of name * fix blank page for newly created roles --- invenio_app_rdm/administration/roles/roles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_app_rdm/administration/roles/roles.py b/invenio_app_rdm/administration/roles/roles.py index 824856059..75bd9ee07 100644 --- a/invenio_app_rdm/administration/roles/roles.py +++ b/invenio_app_rdm/administration/roles/roles.py @@ -24,7 +24,7 @@ class RoleAdminMixin: extension_name = "invenio-users-resources" api_endpoint = "/groups" - pid_path = "name" + pid_path = "id" create_view_name = "roles_create" list_view_name = "roles" From f0d986cfbc734d4b7216e0eff4d175a0d28dabce Mon Sep 17 00:00:00 2001 From: Sam Arbid Date: Mon, 17 Nov 2025 16:30:06 +0100 Subject: [PATCH 5/6] admin: restrict user/role views to user admins * Added a shared can_access_user_administration() helper that only returns true for superusers or identities carrying both administration and administration-moderation. * The users/roles admin list/detail views now inherit a mixin that aborts with 403 whenever that helper denies access, preventing direct navigation by unauthorized users --- invenio_app_rdm/administration/roles/roles.py | 10 ++++++- .../administration/users/permissions.py | 28 +++++++++++++++++++ invenio_app_rdm/administration/users/users.py | 18 ++++++++++-- 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 invenio_app_rdm/administration/users/permissions.py diff --git a/invenio_app_rdm/administration/roles/roles.py b/invenio_app_rdm/administration/roles/roles.py index 75bd9ee07..3a2d0ff82 100644 --- a/invenio_app_rdm/administration/roles/roles.py +++ b/invenio_app_rdm/administration/roles/roles.py @@ -7,7 +7,7 @@ # under the terms of the MIT License; see LICENSE file for more details. """Administration views for managing roles.""" - +from flask import abort from invenio_administration.views.base import ( AdminResourceCreateView, AdminResourceDetailView, @@ -16,6 +16,8 @@ ) from invenio_i18n import lazy_gettext as _ +from ..users.permissions import can_access_user_administration + class RoleAdminMixin: """Shared configuration for role administration views.""" @@ -38,6 +40,12 @@ class RoleAdminMixin: search_sort_config_name = "USERS_RESOURCES_GROUPS_ADMIN_SORT_OPTIONS" search_facets_config_name = "USERS_RESOURCES_GROUPS_ADMIN_FACETS" + def dispatch_request(self, **kwargs): + """Deny direct navigation to unauthorized users.""" + if not can_access_user_administration(): + abort(403) + return super().get(**kwargs) + class RolesListView(RoleAdminMixin, AdminResourceListView): """List roles.""" diff --git a/invenio_app_rdm/administration/users/permissions.py b/invenio_app_rdm/administration/users/permissions.py new file mode 100644 index 000000000..15535e3de --- /dev/null +++ b/invenio_app_rdm/administration/users/permissions.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2025 CERN. +# Copyright (C) 2025 KTH Royal Institute of Technology. +# +# Invenio App RDM is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Helpers for administration moderation access checks.""" + +from flask import g +from flask_principal import RoleNeed +from invenio_access.permissions import superuser_access + + +def can_access_user_administration(identity=None): + """Return True if the identity may access user/role admin views.""" + identity = identity or getattr(g, "identity", None) + if not identity: + return False + + provides = identity.provides + admin_need = RoleNeed("administration") + moderator_need = RoleNeed("administration-moderation") + + return (superuser_access in provides) or ( + admin_need in provides and moderator_need in provides + ) diff --git a/invenio_app_rdm/administration/users/users.py b/invenio_app_rdm/administration/users/users.py index d2888aec0..94aa36578 100644 --- a/invenio_app_rdm/administration/users/users.py +++ b/invenio_app_rdm/administration/users/users.py @@ -10,7 +10,7 @@ from functools import partial -from flask import current_app +from flask import abort, current_app from invenio_administration.views.base import ( AdminResourceDetailView, AdminResourceListView, @@ -18,6 +18,8 @@ from invenio_i18n import lazy_gettext as _ from invenio_search_ui.searchconfig import search_app_config +from .permissions import can_access_user_administration + USERS_ITEM_LIST = { "user": {"text": _("User"), "order": 2, "width": 3}, "username": {"text": _("Username"), "order": 3, "width": 2}, @@ -48,7 +50,17 @@ # List of the columns displayed on the user list and user details -class UsersListView(AdminResourceListView): +class UserAdminAccessMixin: + """Mixin asserting only user admins identities access user views.""" + + def dispatch_request(self, *args, **kwargs): + """Override Flask view to add permission check.""" + if not can_access_user_administration(): + abort(403) + return super().dispatch_request(*args, **kwargs) + + +class UsersListView(UserAdminAccessMixin, AdminResourceListView): """Configuration for users sets list view.""" api_endpoint = "/users/all" @@ -114,7 +126,7 @@ def init_search_config(self): ) -class UsersDetailView(AdminResourceDetailView): +class UsersDetailView(UserAdminAccessMixin, AdminResourceDetailView): """Configuration for users sets detail view.""" url = "/users/" From 33744a9103d89348577759aa8d8b0e18c57eee92 Mon Sep 17 00:00:00 2001 From: Sam Arbid Date: Tue, 18 Nov 2025 11:37:26 +0100 Subject: [PATCH 6/6] fix: reorder fields in roles list view * Adjusted the order of 'id' and 'name' fields in the roles list view for better clarity. --- invenio_app_rdm/administration/roles/roles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invenio_app_rdm/administration/roles/roles.py b/invenio_app_rdm/administration/roles/roles.py index 3a2d0ff82..763b8ebbd 100644 --- a/invenio_app_rdm/administration/roles/roles.py +++ b/invenio_app_rdm/administration/roles/roles.py @@ -58,8 +58,8 @@ class RolesListView(RoleAdminMixin, AdminResourceListView): order = 30 item_field_list = { - "id": {"text": _("ID"), "order": 0, "width": 1}, - "name": {"text": _("Name"), "order": 1, "width": 3}, + "name": {"text": _("Name"), "order": 0, "width": 3}, + "id": {"text": _("ID"), "order": 1, "width": 3}, "description": {"text": _("Description"), "order": 2, "width": 5}, "is_managed": {"text": _("Managed"), "order": 3, "width": 1}, "created": {"text": _("Created"), "order": 4, "width": 2},