removed roles page and updated it to be user role templates editin page

This commit is contained in:
Ida 2026-06-04 13:54:34 +02:00
parent 2c1ed16464
commit dd25fa603d
3 changed files with 185 additions and 17 deletions

View file

@ -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.

View file

@ -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(

View file

@ -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",