From dd25fa603d49259d57d0044e0fccc61def8989c2 Mon Sep 17 00:00:00 2001 From: Ida Date: Thu, 4 Jun 2026 13:54:34 +0200 Subject: [PATCH] removed roles page and updated it to be user role templates editin page --- modules/interfaces/interfaceBootstrap.py | 112 +++++++++++++++++++++-- modules/routes/routeAdminRbacRules.py | 71 ++++++++++++++ modules/system/mainSystem.py | 19 ++-- 3 files changed, 185 insertions(+), 17 deletions(-) diff --git a/modules/interfaces/interfaceBootstrap.py b/modules/interfaces/interfaceBootstrap.py index 1f450d0c..50f8e007 100644 --- a/modules/interfaces/interfaceBootstrap.py +++ b/modules/interfaces/interfaceBootstrap.py @@ -693,8 +693,11 @@ def _deduplicateRoles(db: DatabaseConnector) -> None: logger.info(f"Deduplicated roles: removed {deletedCount} duplicates") # Migration: Fix isSystemRole flags + # Only the three bootstrap templates (admin, user, viewer) may be isSystemRole=True + # with mandateId=None. Everything else must be False. + _BOOTSTRAP_ROLE_LABELS = {"admin", "user", "viewer"} fixedMandateCount = 0 - fixedTemplateCount = 0 + fixedUserTemplateCount = 0 for role in allRoles: # Mandate-level roles should NOT be isSystemRole=True if role.get("mandateId") is not None and role.get("isSystemRole") is True: @@ -703,17 +706,23 @@ def _deduplicateRoles(db: DatabaseConnector) -> None: fixedMandateCount += 1 except Exception as e: logger.warning(f"Failed to fix mandate role {role.get('id')}: {e}") - # Template roles (mandateId=None, no featureCode) MUST be isSystemRole=True - if role.get("mandateId") is None and role.get("featureCode") is None and role.get("isSystemRole") is not True: + # User-created global templates (mandateId=None, not a bootstrap label) must NOT + # be isSystemRole=True. A previous migration incorrectly promoted them. + if ( + role.get("mandateId") is None + and role.get("featureCode") is None + and role.get("isSystemRole") is True + and role.get("roleLabel") not in _BOOTSTRAP_ROLE_LABELS + ): try: - db.recordModify(Role, role.get("id"), {"isSystemRole": True}) - fixedTemplateCount += 1 + db.recordModify(Role, role.get("id"), {"isSystemRole": False}) + fixedUserTemplateCount += 1 except Exception as e: - logger.warning(f"Failed to fix template role {role.get('id')}: {e}") + logger.warning(f"Failed to demote user template role {role.get('id')}: {e}") if fixedMandateCount > 0: logger.info(f"Fixed {fixedMandateCount} mandate-level roles: isSystemRole → False") - if fixedTemplateCount > 0: - logger.info(f"Fixed {fixedTemplateCount} template roles: isSystemRole → True") + if fixedUserTemplateCount > 0: + logger.info(f"Fixed {fixedUserTemplateCount} user-created global templates: isSystemRole → False") def _ensureAllMandatesHaveSystemRoles(db: DatabaseConnector) -> None: @@ -824,6 +833,93 @@ def copySystemRolesToMandate(db: DatabaseConnector, mandateId: str) -> int: return copiedCount +def copyUserRoleTemplateToMandate( + db: DatabaseConnector, + templateRoleId: str, + mandateId: str, + roleLabel: Optional[str] = None, + description: Optional[dict] = None, +) -> dict: + """ + Copy a user role template (global/system, mandateId=None) into a mandate instance role, + including all AccessRules. + + Args: + db: Database connector + templateRoleId: Source template role id + mandateId: Target mandate id + roleLabel: Optional override for instance role label (defaults to template label) + description: Optional override for multilingual description + + Returns: + Created mandate role record as dict + + Raises: + ValueError: If template invalid, duplicate label, or not found + """ + import uuid as _uuid + + templateRecords = db.getRecordset(Role, recordFilter={"id": templateRoleId}) + if not templateRecords: + raise ValueError(f"Template role not found: {templateRoleId}") + + templateRole = templateRecords[0] + if templateRole.get("mandateId") is not None: + raise ValueError("Source role is not a user role template (has mandateId)") + if templateRole.get("featureInstanceId") is not None or templateRole.get("featureCode"): + raise ValueError("Source role is a feature role, not a user role template") + + effectiveLabel = (roleLabel or templateRole.get("roleLabel") or "").strip().lower().replace(" ", "_") + if not effectiveLabel: + raise ValueError("roleLabel is required") + + existingMandateRoles = db.getRecordset( + Role, + recordFilter={"mandateId": mandateId, "featureInstanceId": None}, + ) + existingLabels = {r.get("roleLabel") for r in existingMandateRoles} + if effectiveLabel in existingLabels: + raise ValueError(f"Mandate already has role '{effectiveLabel}'") + + if description is not None: + descValue = coerce_text_multilingual(description) + else: + descValue = coerce_text_multilingual(templateRole.get("description", {})) + + newRoleId = str(_uuid.uuid4()) + newRole = Role( + id=newRoleId, + roleLabel=effectiveLabel, + description=descValue, + mandateId=mandateId, + featureInstanceId=None, + featureCode=None, + isSystemRole=False, + ) + created = db.recordCreate(Role, newRole.model_dump()) + + templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId}) + for rule in templateRules: + newRule = AccessRule( + id=str(_uuid.uuid4()), + roleId=newRoleId, + context=rule.get("context"), + item=rule.get("item"), + view=rule.get("view", False), + read=rule.get("read"), + create=rule.get("create"), + update=rule.get("update"), + delete=rule.get("delete"), + ) + db.recordCreate(AccessRule, newRule.model_dump()) + + logger.info( + f"Copied user role template '{templateRole.get('roleLabel')}' → mandate {mandateId} " + f"as '{effectiveLabel}' with {len(templateRules)} AccessRules" + ) + return created + + def _getRoleId(db: DatabaseConnector, roleLabel: str) -> Optional[str]: """ Get role ID by label, using cache or database lookup. diff --git a/modules/routes/routeAdminRbacRules.py b/modules/routes/routeAdminRbacRules.py index 3eb45f1b..0ed2de97 100644 --- a/modules/routes/routeAdminRbacRules.py +++ b/modules/routes/routeAdminRbacRules.py @@ -1033,6 +1033,77 @@ def create_role( ) +@router.post("/roles/from-template", response_model=Dict[str, Any]) +@limiter.limit("30/minute") +def create_role_from_template( + request: Request, + body: Dict[str, Any] = Body(...), + reqContext: RequestContext = Depends(getRequestContext), +) -> Dict[str, Any]: + """ + Create a mandate-instance role by copying a user role template and its AccessRules. + + Body: + - templateRoleId: str (required) + - mandateId: str (required) + - roleLabel: str (optional override) + - description: dict (optional multilingual override) + """ + try: + templateRoleId = body.get("templateRoleId") + mandateId = body.get("mandateId") + if not templateRoleId or not mandateId: + raise HTTPException( + status_code=400, + detail=routeApiMsg("templateRoleId and mandateId are required"), + ) + + isSysAdmin = reqContext.isPlatformAdmin + adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext) + if not isSysAdmin and not adminMandateIds: + raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required")) + if not isSysAdmin and str(mandateId) not in adminMandateIds: + raise HTTPException( + status_code=403, + detail=routeApiMsg("Access denied: can only create roles in your own mandates"), + ) + + from modules.interfaces.interfaceBootstrap import copyUserRoleTemplateToMandate + + interface = getRootInterface() + created = copyUserRoleTemplateToMandate( + interface.db, + str(templateRoleId), + str(mandateId), + roleLabel=body.get("roleLabel"), + description=body.get("description"), + ) + + desc = created.get("description") + if hasattr(desc, "model_dump"): + desc = desc.model_dump() + + return { + "id": created.get("id"), + "roleLabel": created.get("roleLabel"), + "description": resolveText(desc) if desc else desc, + "mandateId": created.get("mandateId"), + "featureInstanceId": created.get("featureInstanceId"), + "featureCode": created.get("featureCode"), + "isSystemRole": created.get("isSystemRole"), + } + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Error creating role from template: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Failed to create role from template: {str(e)}", + ) + + @router.get("/roles/{roleId}", response_model=Dict[str, Any]) @limiter.limit("60/minute") def get_role( diff --git a/modules/system/mainSystem.py b/modules/system/mainSystem.py index 0bec2946..5460830c 100644 --- a/modules/system/mainSystem.py +++ b/modules/system/mainSystem.py @@ -256,15 +256,6 @@ NAVIGATION_SECTIONS = [ "title": t("System"), "order": 30, "items": [ - { - "id": "admin-roles", - "objectKey": "ui.admin.roles", - "label": t("Rollen"), - "icon": "FaUserTag", - "path": "/admin/mandate-roles", - "order": 10, - "adminOnly": True, - }, { "id": "admin-mandates", "objectKey": "ui.admin.mandates", @@ -301,6 +292,16 @@ NAVIGATION_SECTIONS = [ "order": 60, "adminOnly": True, }, + { + "id": "admin-user-role-templates", + "objectKey": "ui.admin.userRoleTemplates", + "label": t("User Role Templates"), + "icon": "FaUserTag", + "path": "/admin/user-role-templates", + "order": 65, + "adminOnly": True, + "sysAdminOnly": True, + }, { "id": "admin-feature-roles", "objectKey": "ui.admin.featureRoles",