removed roles page and updated it to be user role templates editin page
This commit is contained in:
parent
2c1ed16464
commit
dd25fa603d
3 changed files with 185 additions and 17 deletions
|
|
@ -693,8 +693,11 @@ def _deduplicateRoles(db: DatabaseConnector) -> None:
|
||||||
logger.info(f"Deduplicated roles: removed {deletedCount} duplicates")
|
logger.info(f"Deduplicated roles: removed {deletedCount} duplicates")
|
||||||
|
|
||||||
# Migration: Fix isSystemRole flags
|
# 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
|
fixedMandateCount = 0
|
||||||
fixedTemplateCount = 0
|
fixedUserTemplateCount = 0
|
||||||
for role in allRoles:
|
for role in allRoles:
|
||||||
# Mandate-level roles should NOT be isSystemRole=True
|
# Mandate-level roles should NOT be isSystemRole=True
|
||||||
if role.get("mandateId") is not None and role.get("isSystemRole") is 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
|
fixedMandateCount += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to fix mandate role {role.get('id')}: {e}")
|
logger.warning(f"Failed to fix mandate role {role.get('id')}: {e}")
|
||||||
# Template roles (mandateId=None, no featureCode) MUST be isSystemRole=True
|
# User-created global templates (mandateId=None, not a bootstrap label) must NOT
|
||||||
if role.get("mandateId") is None and role.get("featureCode") is None and role.get("isSystemRole") is not True:
|
# 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:
|
try:
|
||||||
db.recordModify(Role, role.get("id"), {"isSystemRole": True})
|
db.recordModify(Role, role.get("id"), {"isSystemRole": False})
|
||||||
fixedTemplateCount += 1
|
fixedUserTemplateCount += 1
|
||||||
except Exception as e:
|
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:
|
if fixedMandateCount > 0:
|
||||||
logger.info(f"Fixed {fixedMandateCount} mandate-level roles: isSystemRole → False")
|
logger.info(f"Fixed {fixedMandateCount} mandate-level roles: isSystemRole → False")
|
||||||
if fixedTemplateCount > 0:
|
if fixedUserTemplateCount > 0:
|
||||||
logger.info(f"Fixed {fixedTemplateCount} template roles: isSystemRole → True")
|
logger.info(f"Fixed {fixedUserTemplateCount} user-created global templates: isSystemRole → False")
|
||||||
|
|
||||||
|
|
||||||
def _ensureAllMandatesHaveSystemRoles(db: DatabaseConnector) -> None:
|
def _ensureAllMandatesHaveSystemRoles(db: DatabaseConnector) -> None:
|
||||||
|
|
@ -824,6 +833,93 @@ def copySystemRolesToMandate(db: DatabaseConnector, mandateId: str) -> int:
|
||||||
return copiedCount
|
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]:
|
def _getRoleId(db: DatabaseConnector, roleLabel: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Get role ID by label, using cache or database lookup.
|
Get role ID by label, using cache or database lookup.
|
||||||
|
|
|
||||||
|
|
@ -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])
|
@router.get("/roles/{roleId}", response_model=Dict[str, Any])
|
||||||
@limiter.limit("60/minute")
|
@limiter.limit("60/minute")
|
||||||
def get_role(
|
def get_role(
|
||||||
|
|
|
||||||
|
|
@ -256,15 +256,6 @@ NAVIGATION_SECTIONS = [
|
||||||
"title": t("System"),
|
"title": t("System"),
|
||||||
"order": 30,
|
"order": 30,
|
||||||
"items": [
|
"items": [
|
||||||
{
|
|
||||||
"id": "admin-roles",
|
|
||||||
"objectKey": "ui.admin.roles",
|
|
||||||
"label": t("Rollen"),
|
|
||||||
"icon": "FaUserTag",
|
|
||||||
"path": "/admin/mandate-roles",
|
|
||||||
"order": 10,
|
|
||||||
"adminOnly": True,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "admin-mandates",
|
"id": "admin-mandates",
|
||||||
"objectKey": "ui.admin.mandates",
|
"objectKey": "ui.admin.mandates",
|
||||||
|
|
@ -301,6 +292,16 @@ NAVIGATION_SECTIONS = [
|
||||||
"order": 60,
|
"order": 60,
|
||||||
"adminOnly": True,
|
"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",
|
"id": "admin-feature-roles",
|
||||||
"objectKey": "ui.admin.featureRoles",
|
"objectKey": "ui.admin.featureRoles",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue