gateway/modules/features/redmine/mainRedmine.py
2026-04-21 21:30:11 +02:00

335 lines
14 KiB
Python

# Copyright (c) 2026 Patrick Motsch
# All rights reserved.
"""Redmine Feature Container -- Main Module.
Defines the feature metadata and registers RBAC objects + template roles
in the catalog. Loaded automatically by ``modules.system.registry``.
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List
from modules.shared.i18nRegistry import t
logger = logging.getLogger(__name__)
FEATURE_CODE = "redmine"
FEATURE_LABEL = t("Redmine", context="UI")
FEATURE_ICON = "mdi-bug-outline"
# Wrapping labels in t() at import time registers the keys with the i18n
# catalog immediately, so the AI translator picks them up on the next sweep.
# Without this, brand-new labels like "Ticket-Browser" stay untranslated and
# render as ``[Ticket-Browser]`` in non-de UIs.
UI_OBJECTS: List[Dict[str, Any]] = [
{"objectKey": "ui.feature.redmine.stats", "label": t("Statistik", context="UI"), "meta": {"area": "stats", "isDefault": True}},
{"objectKey": "ui.feature.redmine.browser", "label": t("Ticket-Browser", context="UI"), "meta": {"area": "browser"}},
{"objectKey": "ui.feature.redmine.settings", "label": t("Einstellungen", context="UI"), "meta": {"area": "settings", "admin_only": True}},
]
DATA_OBJECTS: List[Dict[str, Any]] = [
{"objectKey": "data.feature.redmine.config", "label": "Konfiguration", "meta": {"isGroup": True}},
{
"objectKey": "data.feature.redmine.RedmineInstanceConfig",
"label": "Redmine-Verbindung",
"meta": {
"table": "RedmineInstanceConfig",
"group": "data.feature.redmine.config",
"fields": ["id", "baseUrl", "projectId", "rootTrackerName", "isActive", "lastConnectedAt", "lastSyncAt"],
},
},
{
"objectKey": "data.feature.redmine.RedmineTicketMirror",
"label": "Redmine-Tickets (Mirror)",
"meta": {
"table": "RedmineTicketMirror",
"group": "data.feature.redmine.config",
"fields": ["redmineId", "subject", "trackerName", "statusName", "assignedToName", "updatedOn"],
},
},
{
"objectKey": "data.feature.redmine.RedmineRelationMirror",
"label": "Redmine-Beziehungen (Mirror)",
"meta": {
"table": "RedmineRelationMirror",
"group": "data.feature.redmine.config",
"fields": ["redmineRelationId", "issueId", "issueToId", "relationType"],
},
},
{
"objectKey": "data.feature.redmine.*",
"label": "Alle Redmine-Daten",
"meta": {"wildcard": True, "description": "Wildcard for all redmine data tables"},
},
]
RESOURCE_OBJECTS: List[Dict[str, Any]] = [
{
"objectKey": "resource.feature.redmine.tickets.read",
"label": "Tickets lesen",
"meta": {"endpoint": "/api/redmine/{instanceId}/tickets", "method": "GET"},
},
{
"objectKey": "resource.feature.redmine.tickets.create",
"label": "Tickets erstellen",
"meta": {"endpoint": "/api/redmine/{instanceId}/tickets", "method": "POST"},
},
{
"objectKey": "resource.feature.redmine.tickets.update",
"label": "Tickets bearbeiten",
"meta": {"endpoint": "/api/redmine/{instanceId}/tickets/{issueId}", "method": "PUT"},
},
{
"objectKey": "resource.feature.redmine.tickets.delete",
"label": "Tickets loeschen / archivieren",
"meta": {"endpoint": "/api/redmine/{instanceId}/tickets/{issueId}", "method": "DELETE"},
},
{
"objectKey": "resource.feature.redmine.relations.manage",
"label": "Beziehungen verwalten",
"meta": {"endpoint": "/api/redmine/{instanceId}/tickets/{issueId}/relations", "method": "ALL"},
},
{
"objectKey": "resource.feature.redmine.stats.read",
"label": "Statistik einsehen",
"meta": {"endpoint": "/api/redmine/{instanceId}/stats", "method": "GET"},
},
{
"objectKey": "resource.feature.redmine.config.manage",
"label": "Verbindung verwalten",
"meta": {"endpoint": "/api/redmine/{instanceId}/config", "method": "ALL", "admin_only": True},
},
{
"objectKey": "resource.feature.redmine.config.test",
"label": "Verbindung testen",
"meta": {"endpoint": "/api/redmine/{instanceId}/config/test", "method": "POST", "admin_only": True},
},
{
"objectKey": "resource.feature.redmine.sync.run",
"label": "Mirror synchronisieren",
"meta": {"endpoint": "/api/redmine/{instanceId}/sync", "method": "POST", "admin_only": True},
},
{
"objectKey": "resource.feature.redmine.sync.status",
"label": "Sync-Status lesen",
"meta": {"endpoint": "/api/redmine/{instanceId}/sync/status", "method": "GET"},
},
{
"objectKey": "resource.feature.redmine.workflows.view",
"label": "Workflows einsehen",
"meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "GET"},
},
{
"objectKey": "resource.feature.redmine.workflows.execute",
"label": "Workflows ausfuehren",
"meta": {"endpoint": "/api/workflows/{instanceId}/execute", "method": "POST"},
},
]
TEMPLATE_ROLES: List[Dict[str, Any]] = [
{
"roleLabel": "redmine-viewer",
"description": "Redmine-Betrachter -- Tickets und Statistik lesen",
"accessRules": [
{"context": "UI", "item": "ui.feature.redmine.stats", "view": True},
{"context": "UI", "item": "ui.feature.redmine.browser", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.tickets.read", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.stats.read", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.sync.status", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
],
},
{
"roleLabel": "redmine-editor",
"description": "Redmine-Bearbeiter -- Tickets erstellen, bearbeiten, Beziehungen pflegen",
"accessRules": [
{"context": "UI", "item": "ui.feature.redmine.stats", "view": True},
{"context": "UI", "item": "ui.feature.redmine.browser", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.tickets.read", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.tickets.create", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.tickets.update", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.tickets.delete", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.relations.manage", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.stats.read", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.sync.status", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.workflows.view", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.workflows.execute", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "n"},
],
},
{
"roleLabel": "redmine-admin",
"description": "Redmine-Administrator -- Vollzugriff inkl. Einstellungen und Verbindung",
"accessRules": [
{"context": "UI", "item": None, "view": True},
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
{"context": "RESOURCE", "item": "resource.feature.redmine.config.manage", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.config.test", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.tickets.create", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.tickets.update", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.tickets.delete", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.relations.manage", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.workflows.view", "view": True},
{"context": "RESOURCE", "item": "resource.feature.redmine.workflows.execute", "view": True},
],
},
]
# ---------------------------------------------------------------------------
# Public discovery API (called by registry.py)
# ---------------------------------------------------------------------------
def getFeatureDefinition() -> Dict[str, Any]:
return {"code": FEATURE_CODE, "label": FEATURE_LABEL, "icon": FEATURE_ICON}
def getUiObjects() -> List[Dict[str, Any]]:
return UI_OBJECTS
def getResourceObjects() -> List[Dict[str, Any]]:
return RESOURCE_OBJECTS
def getDataObjects() -> List[Dict[str, Any]]:
return DATA_OBJECTS
def getTemplateRoles() -> List[Dict[str, Any]]:
return TEMPLATE_ROLES
def getTemplateWorkflows() -> List[Dict[str, Any]]:
return []
def registerFeature(catalogService) -> bool:
"""Register UI / Resource / Data objects and sync template roles."""
try:
for uiObj in UI_OBJECTS:
catalogService.registerUiObject(
featureCode=FEATURE_CODE,
objectKey=uiObj["objectKey"],
label=uiObj["label"],
meta=uiObj.get("meta"),
)
for resObj in RESOURCE_OBJECTS:
catalogService.registerResourceObject(
featureCode=FEATURE_CODE,
objectKey=resObj["objectKey"],
label=resObj["label"],
meta=resObj.get("meta"),
)
for dataObj in DATA_OBJECTS:
catalogService.registerDataObject(
featureCode=FEATURE_CODE,
objectKey=dataObj["objectKey"],
label=dataObj["label"],
meta=dataObj.get("meta"),
)
_syncTemplateRolesToDb()
logger.info(
f"Feature '{FEATURE_CODE}' registered "
f"{len(UI_OBJECTS)} UI, {len(RESOURCE_OBJECTS)} resource, {len(DATA_OBJECTS)} data objects"
)
return True
except Exception as e:
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False
# ---------------------------------------------------------------------------
# Template-role sync (mirrors the trustee implementation)
# ---------------------------------------------------------------------------
def _syncTemplateRolesToDb() -> int:
try:
from modules.datamodels.datamodelRbac import (
AccessRule,
AccessRuleContext,
Role,
)
from modules.datamodels.datamodelUtils import coerce_text_multilingual
from modules.interfaces.interfaceDbApp import getRootInterface
rootInterface = getRootInterface()
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
templateRoles = [r for r in existingRoles if r.mandateId is None]
existingByLabel: Dict[str, str] = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
roleLabel = roleTemplate["roleLabel"]
if roleLabel in existingByLabel:
_ensureAccessRulesForRole(
rootInterface,
existingByLabel[roleLabel],
roleTemplate.get("accessRules", []),
)
continue
newRole = Role(
roleLabel=roleLabel,
description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None,
featureInstanceId=None,
isSystemRole=False,
)
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
return createdCount
except Exception as e:
logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
return 0
def _ensureAccessRulesForRole(
rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]
) -> int:
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
existingRules = rootInterface.getAccessRulesByRole(roleId)
existingSignatures: set[Any] = set()
for rule in existingRules:
sig = (rule.context.value if rule.context else None, rule.item)
existingSignatures.add(sig)
createdCount = 0
for template in ruleTemplates:
context = template.get("context", "UI")
item = template.get("item")
if (context, item) in existingSignatures:
continue
if context == "UI":
contextEnum = AccessRuleContext.UI
elif context == "DATA":
contextEnum = AccessRuleContext.DATA
elif context == "RESOURCE":
contextEnum = AccessRuleContext.RESOURCE
else:
contextEnum = context
newRule = AccessRule(
roleId=roleId,
context=contextEnum,
item=item,
view=template.get("view", False),
read=template.get("read"),
create=template.get("create"),
update=template.get("update"),
delete=template.get("delete"),
)
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
createdCount += 1
return createdCount