cleaned up all route and main references - no direct access to db.getRecordset - only over interfaces

This commit is contained in:
ValueOn AG 2026-02-04 14:09:35 +01:00
parent 555c9429fb
commit 45eda1e4d4
46 changed files with 2462 additions and 1316 deletions

7
app.py
View file

@ -485,7 +485,6 @@ app.include_router(rbacAdminRulesRouter)
from modules.routes.routeMessaging import router as messagingRouter
app.include_router(messagingRouter)
# Phase 8: New Feature Routes
from modules.routes.routeAdminFeatures import router as featuresAdminRouter
app.include_router(featuresAdminRouter)
@ -504,12 +503,6 @@ app.include_router(userAccessOverviewRouter)
from modules.routes.routeGdpr import router as gdprRouter
app.include_router(gdprRouter)
from modules.routes.routeChat import router as chatRouter
app.include_router(chatRouter)
from modules.features.chatbot.routeFeatureChatbot import router as chatbotFeatureRouter
app.include_router(chatbotFeatureRouter)
# ============================================================================
# SYSTEM ROUTES (Navigation, etc.)
# ============================================================================

View file

@ -72,7 +72,7 @@ class AiAnthropic(BaseConnectorAi):
(OperationTypeEnum.DATA_EXTRACT, 8)
),
version="claude-sonnet-4-5-20250929",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
),
AiModel(
name="claude-sonnet-4-5-20250929",
@ -93,7 +93,7 @@ class AiAnthropic(BaseConnectorAi):
(OperationTypeEnum.IMAGE_ANALYSE, 10)
),
version="claude-sonnet-4-5-20250929",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
)
]

View file

@ -40,7 +40,7 @@ class AiInternal(BaseConnectorAi):
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(),
version="internal-extractor-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.001 + (bytesSent + bytesReceived) / (1024 * 1024) * 0.01
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: 0.001 + (bytesSent + bytesReceived) / (1024 * 1024) * 0.01
),
AiModel(
name="internal-generator",
@ -60,7 +60,7 @@ class AiInternal(BaseConnectorAi):
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(),
version="internal-generator-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.002 + (bytesReceived / (1024 * 1024)) * 0.005
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: 0.002 + (bytesReceived / (1024 * 1024)) * 0.005
),
AiModel(
name="internal-renderer",
@ -80,7 +80,7 @@ class AiInternal(BaseConnectorAi):
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(),
version="internal-renderer-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.003 + (bytesReceived / (1024 * 1024)) * 0.008
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: 0.003 + (bytesReceived / (1024 * 1024)) * 0.008
)
]

View file

@ -72,7 +72,7 @@ class AiOpenai(BaseConnectorAi):
(OperationTypeEnum.DATA_EXTRACT, 7)
),
version="gpt-4o",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
),
AiModel(
name="gpt-3.5-turbo",
@ -97,7 +97,7 @@ class AiOpenai(BaseConnectorAi):
# Note: GPT-3.5-turbo does NOT support vision/image operations
),
version="gpt-3.5-turbo",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0015 + (bytesReceived / 4 / 1000) * 0.002
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0015 + (bytesReceived / 4 / 1000) * 0.002
),
AiModel(
name="gpt-4o",
@ -118,7 +118,7 @@ class AiOpenai(BaseConnectorAi):
(OperationTypeEnum.IMAGE_ANALYSE, 9)
),
version="gpt-4o",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
),
AiModel(
name="dall-e-3",
@ -140,7 +140,7 @@ class AiOpenai(BaseConnectorAi):
(OperationTypeEnum.IMAGE_GENERATE, 10)
),
version="dall-e-3",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
)
]

View file

@ -74,7 +74,7 @@ class AiPerplexity(BaseConnectorAi):
(OperationTypeEnum.WEB_CRAWL, 7)
),
version="sonar",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
),
AiModel(
name="sonar-pro",
@ -97,7 +97,7 @@ class AiPerplexity(BaseConnectorAi):
(OperationTypeEnum.WEB_CRAWL, 8)
),
version="sonar-pro",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
)
]

View file

@ -71,7 +71,7 @@ class AiTavily(BaseConnectorAi):
(OperationTypeEnum.WEB_CRAWL, 10)
),
version="tavily-search",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.008 # Simple flat rate
calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: 0.008 # Simple flat rate
)
]

View file

@ -98,7 +98,7 @@ class AiModel(BaseModel):
# Function reference (not serialized)
functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model")
calculatePriceUsd: Optional[Callable] = Field(default=None, exclude=True, description="Function to calculate price in USD")
calculatepriceCHF: Optional[Callable] = Field(default=None, exclude=True, description="Function to calculate price in USD")
# Selection criteria - capabilities with ratings
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Default priority for this model. See PriorityEnum for available values.")
@ -159,7 +159,7 @@ class AiCallResponse(BaseModel):
content: str = Field(description="AI response content")
modelName: str = Field(description="Selected model name")
priceUsd: float = Field(default=0.0, description="Calculated price in USD")
priceCHF: float = Field(default=0.0, description="Calculated price in USD")
processingTime: float = Field(default=0.0, description="Duration in seconds")
bytesSent: int = Field(default=0, description="Input data size in bytes")
bytesReceived: int = Field(default=0, description="Output data size in bytes")

View file

@ -26,7 +26,7 @@ class ChatStat(BaseModel):
errorCount: Optional[int] = Field(None, description="Number of errors encountered")
process: Optional[str] = Field(None, description="The process that delivers the stats data (e.g. 'action.outlook.readMails', 'ai.process.document.name')")
engine: Optional[str] = Field(None, description="The engine used (e.g. 'ai.anthropic.35', 'ai.tavily.basic', 'renderer.docx')")
priceUsd: Optional[float] = Field(None, description="Calculated price in USD for the operation")
priceCHF: Optional[float] = Field(None, description="Calculated price in USD for the operation")
registerModelLabels(
@ -41,7 +41,7 @@ registerModelLabels(
"errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"},
"process": {"en": "Process", "fr": "Processus"},
"engine": {"en": "Engine", "fr": "Moteur"},
"priceUsd": {"en": "Price USD", "fr": "Prix USD"},
"priceCHF": {"en": "Price USD", "fr": "Prix USD"},
},
)

View file

@ -59,7 +59,24 @@ RESOURCE_OBJECTS = [
]
# Template roles for this feature
# IMPORTANT: "viewer" role is required for automatic user assignment!
TEMPLATE_ROLES = [
{
"roleLabel": "viewer",
"description": {
"en": "Automation Viewer - View automations and execution results",
"de": "Automatisierungs-Betrachter - Automatisierungen und Ausführungsergebnisse einsehen",
"fr": "Visualiseur automatisation - Consulter les automatisations et résultats"
},
"accessRules": [
# UI access to all views
{"context": "UI", "item": "ui.feature.automation.definitions", "view": True},
{"context": "UI", "item": "ui.feature.automation.templates", "view": True},
{"context": "UI", "item": "ui.feature.automation.logs", "view": True},
# Read-only DATA access
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
]
},
{
"roleLabel": "automation-admin",
"description": {
@ -161,9 +178,132 @@ def registerFeature(catalogService) -> bool:
meta=resObj.get("meta")
)
# Sync template roles to database
_syncTemplateRolesToDb()
logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
return True
except Exception as e:
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False
def _syncTemplateRolesToDb() -> int:
"""
Sync template roles and their AccessRules to the database.
Creates global template roles (mandateId=None) if they don't exist.
Returns:
Number of roles created/updated
"""
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
rootInterface = getRootInterface()
# Get existing template roles for this feature (Pydantic models)
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
# Filter to template roles (mandateId is None)
templateRoles = [r for r in existingRoles if r.mandateId is None]
existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
roleLabel = roleTemplate["roleLabel"]
if roleLabel in existingRoleLabels:
roleId = existingRoleLabels[roleLabel]
logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
# Ensure AccessRules exist for this role
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
else:
# Create new template role
newRole = Role(
roleLabel=roleLabel,
description=roleTemplate.get("description", {}),
featureCode=FEATURE_CODE,
mandateId=None, # Global template
featureInstanceId=None,
isSystemRole=False
)
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
# Create AccessRules for this role
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
if createdCount > 0:
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
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:
"""
Ensure AccessRules exist for a role based on templates.
Args:
rootInterface: Root interface instance
roleId: Role ID
ruleTemplates: List of rule templates
Returns:
Number of rules created
"""
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
# Get existing rules for this role (Pydantic models)
existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
existingSignatures = set()
for rule in existingRules:
sig = (str(rule.context) 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")
sig = (context, item)
if sig in existingSignatures:
continue
# Map context string to enum
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
if createdCount > 0:
logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
return createdCount

View file

@ -0,0 +1,6 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Chat Playground Feature Container.
Provides workflow-based chat playground functionality.
"""

View file

@ -0,0 +1,145 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Chat Playground Feature Interface.
Wrapper around interfaceDbChat with feature instance context.
"""
import logging
from typing import Dict, Any, List, Optional
from modules.datamodels.datamodelUam import User
from modules.interfaces import interfaceDbChat
logger = logging.getLogger(__name__)
# Feature code constant
FEATURE_CODE = "chatplayground"
# Singleton instances cache
_instances: Dict[str, "ChatPlaygroundObjects"] = {}
def getInterface(currentUser: User, mandateId: str = None, featureInstanceId: str = None) -> "ChatPlaygroundObjects":
"""
Factory function to get or create a ChatPlaygroundObjects instance.
Uses singleton pattern per user context.
Args:
currentUser: Current user object
mandateId: Mandate ID
featureInstanceId: Feature instance ID
Returns:
ChatPlaygroundObjects instance
"""
cacheKey = f"{currentUser.id}_{mandateId}_{featureInstanceId}"
if cacheKey not in _instances:
_instances[cacheKey] = ChatPlaygroundObjects(currentUser, mandateId, featureInstanceId)
else:
# Update context if needed
_instances[cacheKey].setUserContext(currentUser, mandateId, featureInstanceId)
return _instances[cacheKey]
class ChatPlaygroundObjects:
"""
Chat Playground feature interface.
Wraps the shared interfaceDbChat with feature instance context.
"""
FEATURE_CODE = FEATURE_CODE
def __init__(self, currentUser: User, mandateId: str = None, featureInstanceId: str = None):
"""
Initialize the Chat Playground interface.
Args:
currentUser: Current user object
mandateId: Mandate ID
featureInstanceId: Feature instance ID
"""
self.currentUser = currentUser
self.mandateId = mandateId
self.featureInstanceId = featureInstanceId
# Get the underlying chat interface
self._chatInterface = interfaceDbChat.getInterface(
currentUser,
mandateId=mandateId,
featureInstanceId=featureInstanceId
)
def setUserContext(self, currentUser: User, mandateId: str = None, featureInstanceId: str = None):
"""
Update the user context.
Args:
currentUser: Current user object
mandateId: Mandate ID
featureInstanceId: Feature instance ID
"""
self.currentUser = currentUser
self.mandateId = mandateId
self.featureInstanceId = featureInstanceId
# Update underlying interface
self._chatInterface = interfaceDbChat.getInterface(
currentUser,
mandateId=mandateId,
featureInstanceId=featureInstanceId
)
# =========================================================================
# Delegated methods from interfaceDbChat
# =========================================================================
def getWorkflow(self, workflowId: str) -> Optional[Dict[str, Any]]:
"""Get a workflow by ID."""
return self._chatInterface.getWorkflow(workflowId)
def getWorkflows(self, pagination=None) -> Dict[str, Any]:
"""Get all workflows with pagination."""
return self._chatInterface.getWorkflows(pagination=pagination)
def getUnifiedChatData(self, workflowId: str, afterTimestamp: float = None) -> Dict[str, Any]:
"""Get unified chat data for a workflow."""
return self._chatInterface.getUnifiedChatData(workflowId, afterTimestamp)
def createWorkflow(self, workflow) -> Dict[str, Any]:
"""Create a new workflow."""
return self._chatInterface.createWorkflow(workflow)
def updateWorkflow(self, workflowId: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Update a workflow."""
return self._chatInterface.updateWorkflow(workflowId, updates)
def deleteWorkflow(self, workflowId: str) -> bool:
"""Delete a workflow."""
return self._chatInterface.deleteWorkflow(workflowId)
def getMessages(self, workflowId: str) -> List[Dict[str, Any]]:
"""Get messages for a workflow."""
return self._chatInterface.getMessages(workflowId)
def createMessage(self, message) -> Dict[str, Any]:
"""Create a new message."""
return self._chatInterface.createMessage(message)
def getLogs(self, workflowId: str) -> List[Dict[str, Any]]:
"""Get logs for a workflow."""
return self._chatInterface.getLogs(workflowId)
def createLog(self, log) -> Dict[str, Any]:
"""Create a new log entry."""
return self._chatInterface.createLog(log)
def getStats(self, workflowId: str) -> List[Dict[str, Any]]:
"""Get stats for a workflow."""
return self._chatInterface.getStats(workflowId)
def createStat(self, stat) -> Dict[str, Any]:
"""Create a new stat entry."""
return self._chatInterface.createStat(stat)

View file

@ -0,0 +1,273 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Chat Playground Feature Container - Main Module.
Handles feature initialization and RBAC catalog registration.
"""
import logging
from typing import Dict, List, Any
logger = logging.getLogger(__name__)
# Feature metadata
FEATURE_CODE = "chatplayground"
FEATURE_LABEL = {"en": "Chat Playground", "de": "Chat Playground", "fr": "Chat Playground"}
FEATURE_ICON = "mdi-message-text"
# UI Objects for RBAC catalog
UI_OBJECTS = [
{
"objectKey": "ui.feature.chatplayground.playground",
"label": {"en": "Playground", "de": "Playground", "fr": "Playground"},
"meta": {"area": "playground"}
},
{
"objectKey": "ui.feature.chatplayground.workflows",
"label": {"en": "Workflows", "de": "Workflows", "fr": "Workflows"},
"meta": {"area": "workflows"}
},
]
# Resource Objects for RBAC catalog
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.chatplayground.start",
"label": {"en": "Start Workflow", "de": "Workflow starten", "fr": "Démarrer workflow"},
"meta": {"endpoint": "/api/chatplayground/{instanceId}/start", "method": "POST"}
},
{
"objectKey": "resource.feature.chatplayground.stop",
"label": {"en": "Stop Workflow", "de": "Workflow stoppen", "fr": "Arrêter workflow"},
"meta": {"endpoint": "/api/chatplayground/{instanceId}/{workflowId}/stop", "method": "POST"}
},
{
"objectKey": "resource.feature.chatplayground.chatData",
"label": {"en": "Get Chat Data", "de": "Chat-Daten abrufen", "fr": "Récupérer données chat"},
"meta": {"endpoint": "/api/chatplayground/{instanceId}/{workflowId}/chatData", "method": "GET"}
},
]
# Template roles for this feature
# IMPORTANT: "viewer" role is required for automatic user assignment!
TEMPLATE_ROLES = [
{
"roleLabel": "viewer",
"description": {
"en": "Chat Playground Viewer - View and use chat playground",
"de": "Chat Playground Betrachter - Chat Playground ansehen und nutzen",
"fr": "Visualiseur Chat Playground - Consulter et utiliser le chat playground"
},
"accessRules": [
# UI access to all views
{"context": "UI", "item": "ui.feature.chatplayground.playground", "view": True},
{"context": "UI", "item": "ui.feature.chatplayground.workflows", "view": True},
# Resource access
{"context": "RESOURCE", "item": "resource.feature.chatplayground.start", "view": True},
{"context": "RESOURCE", "item": "resource.feature.chatplayground.stop", "view": True},
{"context": "RESOURCE", "item": "resource.feature.chatplayground.chatData", "view": True},
# DATA access (own records)
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"},
]
},
{
"roleLabel": "admin",
"description": {
"en": "Chat Playground Admin - Full access to chat playground",
"de": "Chat Playground Admin - Vollzugriff auf Chat Playground",
"fr": "Administrateur Chat Playground - Accès complet au chat playground"
},
"accessRules": [
# Full UI access
{"context": "UI", "item": None, "view": True},
# Full resource access
{"context": "RESOURCE", "item": None, "view": True},
# Full DATA access
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
]
},
]
def getFeatureDefinition() -> Dict[str, Any]:
"""Return the feature definition for registration."""
return {
"code": FEATURE_CODE,
"label": FEATURE_LABEL,
"icon": FEATURE_ICON
}
def getUiObjects() -> List[Dict[str, Any]]:
"""Return UI objects for RBAC catalog registration."""
return UI_OBJECTS
def getResourceObjects() -> List[Dict[str, Any]]:
"""Return resource objects for RBAC catalog registration."""
return RESOURCE_OBJECTS
def getTemplateRoles() -> List[Dict[str, Any]]:
"""Return template roles for this feature."""
return TEMPLATE_ROLES
def registerFeature(catalogService) -> bool:
"""
Register this feature's RBAC objects in the catalog.
Args:
catalogService: The RBAC catalog service instance
Returns:
True if registration was successful
"""
try:
# Register UI objects
for uiObj in UI_OBJECTS:
catalogService.registerUiObject(
featureCode=FEATURE_CODE,
objectKey=uiObj["objectKey"],
label=uiObj["label"],
meta=uiObj.get("meta")
)
# Register Resource objects
for resObj in RESOURCE_OBJECTS:
catalogService.registerResourceObject(
featureCode=FEATURE_CODE,
objectKey=resObj["objectKey"],
label=resObj["label"],
meta=resObj.get("meta")
)
# Sync template roles to database
_syncTemplateRolesToDb()
logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
return True
except Exception as e:
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False
def _syncTemplateRolesToDb() -> int:
"""
Sync template roles and their AccessRules to the database.
Creates global template roles (mandateId=None) if they don't exist.
Returns:
Number of roles created/updated
"""
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
rootInterface = getRootInterface()
# Get existing template roles for this feature (Pydantic models)
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
# Filter to template roles (mandateId is None)
templateRoles = [r for r in existingRoles if r.mandateId is None]
existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
roleLabel = roleTemplate["roleLabel"]
if roleLabel in existingRoleLabels:
roleId = existingRoleLabels[roleLabel]
logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
# Ensure AccessRules exist for this role
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
else:
# Create new template role
newRole = Role(
roleLabel=roleLabel,
description=roleTemplate.get("description", {}),
featureCode=FEATURE_CODE,
mandateId=None, # Global template
featureInstanceId=None,
isSystemRole=False
)
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
# Create AccessRules for this role
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
if createdCount > 0:
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
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:
"""
Ensure AccessRules exist for a role based on templates.
Args:
rootInterface: Root interface instance
roleId: Role ID
ruleTemplates: List of rule templates
Returns:
Number of rules created
"""
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
# Get existing rules for this role (Pydantic models)
existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
existingSignatures = set()
for rule in existingRules:
sig = (str(rule.context) 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")
sig = (context, item)
if sig in existingSignatures:
continue
# Map context string to enum
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
if createdCount > 0:
logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
return createdCount

View file

@ -0,0 +1,233 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Chat Playground Feature Routes.
Implements the endpoints for chat playground workflow management as a feature.
"""
import logging
from typing import Optional, Dict, Any
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Request
# Import auth modules
from modules.auth import limiter, getRequestContext, RequestContext
# Import interfaces
from modules.interfaces import interfaceDbChat
# Import models
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
# Import workflow control functions
from modules.workflows.automation import chatStart, chatStop
# Configure logger
logger = logging.getLogger(__name__)
# Create router for chat playground feature endpoints
router = APIRouter(
prefix="/api/chatplayground",
tags=["Chat Playground Feature"],
responses={404: {"description": "Not found"}}
)
def _getServiceChat(context: RequestContext, featureInstanceId: str = None):
"""Get chat interface with feature instance context."""
return interfaceDbChat.getInterface(
context.user,
mandateId=str(context.mandateId) if context.mandateId else None,
featureInstanceId=featureInstanceId
)
async def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
"""
Validate that user has access to the feature instance.
Args:
instanceId: Feature instance ID
context: Request context
Returns:
mandateId for the instance
Raises:
HTTPException if access is denied
"""
from modules.interfaces.interfaceDbApp import getRootInterface
rootInterface = getRootInterface()
# Get feature instance (Pydantic model)
instance = rootInterface.getFeatureInstance(instanceId)
if not instance:
raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
# Check user has access to this instance using interface method
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
if not featureAccess or not featureAccess.enabled:
raise HTTPException(status_code=403, detail="Access denied to this feature instance")
return str(instance.mandateId) if instance.mandateId else None
# Workflow start endpoint
@router.post("/{instanceId}/start", response_model=ChatWorkflow)
@limiter.limit("120/minute")
async def start_workflow(
request: Request,
instanceId: str = Path(..., description="Feature instance ID"),
workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
workflowMode: WorkflowModeEnum = Query(..., description="Workflow mode: 'Dynamic' or 'Automation' (mandatory)"),
userInput: UserInputRequest = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> ChatWorkflow:
"""
Starts a new workflow or continues an existing one.
Args:
instanceId: Feature instance ID
workflowMode: "Dynamic" for iterative dynamic-style processing, "Automation" for automated workflow execution
"""
try:
# Validate access and get mandate ID
mandateId = await _validateInstanceAccess(instanceId, context)
# Start or continue workflow
workflow = await chatStart(
context.user,
userInput,
workflowMode,
workflowId,
mandateId=mandateId,
featureInstanceId=instanceId
)
return workflow
except HTTPException:
raise
except Exception as e:
logger.error(f"Error in start_workflow: {str(e)}")
raise HTTPException(
status_code=500,
detail=str(e)
)
# Stop workflow endpoint
@router.post("/{instanceId}/{workflowId}/stop", response_model=ChatWorkflow)
@limiter.limit("120/minute")
async def stop_workflow(
request: Request,
instanceId: str = Path(..., description="Feature instance ID"),
workflowId: str = Path(..., description="ID of the workflow to stop"),
context: RequestContext = Depends(getRequestContext)
) -> ChatWorkflow:
"""Stops a running workflow."""
try:
# Validate access and get mandate ID
mandateId = await _validateInstanceAccess(instanceId, context)
# Stop workflow
workflow = await chatStop(
context.user,
workflowId,
mandateId=mandateId,
featureInstanceId=instanceId
)
return workflow
except HTTPException:
raise
except Exception as e:
logger.error(f"Error in stop_workflow: {str(e)}")
raise HTTPException(
status_code=500,
detail=str(e)
)
# Unified Chat Data Endpoint for Polling
@router.get("/{instanceId}/{workflowId}/chatData")
@limiter.limit("120/minute")
async def get_workflow_chat_data(
request: Request,
instanceId: str = Path(..., description="Feature instance ID"),
workflowId: str = Path(..., description="ID of the workflow"),
afterTimestamp: Optional[float] = Query(None, description="Unix timestamp to get data after"),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""
Get unified chat data (messages, logs, stats) for a workflow with timestamp-based selective data transfer.
Returns all data types in chronological order based on _createdAt timestamp.
"""
try:
# Validate access
await _validateInstanceAccess(instanceId, context)
# Get service with feature instance context
chatInterface = _getServiceChat(context, featureInstanceId=instanceId)
# Verify workflow exists
workflow = chatInterface.getWorkflow(workflowId)
if not workflow:
raise HTTPException(
status_code=404,
detail=f"Workflow with ID {workflowId} not found"
)
# Get unified chat data
chatData = chatInterface.getUnifiedChatData(workflowId, afterTimestamp)
return chatData
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting unified chat data: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Error getting unified chat data: {str(e)}"
)
# Get workflows for this instance
@router.get("/{instanceId}/workflows")
@limiter.limit("120/minute")
async def get_workflows(
request: Request,
instanceId: str = Path(..., description="Feature instance ID"),
page: int = Query(1, ge=1, description="Page number"),
pageSize: int = Query(20, ge=1, le=100, description="Items per page"),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""
Get all workflows for this feature instance.
"""
try:
# Validate access
await _validateInstanceAccess(instanceId, context)
# Get service with feature instance context
chatInterface = _getServiceChat(context, featureInstanceId=instanceId)
# Get workflows with pagination
from modules.datamodels.datamodelPagination import PaginationParams
pagination = PaginationParams(page=page, pageSize=pageSize)
result = chatInterface.getWorkflows(pagination=pagination)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting workflows: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Error getting workflows: {str(e)}"
)

View file

@ -182,20 +182,18 @@ class SharepointProcessor:
async def _getSharepointConnection(self, sharepointPath: str = None):
try:
connections = self.services.interfaceDbApp.db.getRecordset(
UserConnection,
recordFilter={"userId": self.services.interfaceDbApp.userId}
)
msftConnections = [c for c in connections if c.get('authority') == 'msft']
# Use interface method to get user connections
connections = self.services.interfaceDbApp.getUserConnections(self.services.interfaceDbApp.userId)
msftConnections = [c for c in connections if c.authority == 'msft']
if not msftConnections:
logger.warning('No Microsoft connections found for user')
return None
if len(msftConnections) == 1:
logger.info(f"Found single Microsoft connection: {msftConnections[0].get('id')}")
logger.info(f"Found single Microsoft connection: {msftConnections[0].id}")
return msftConnections[0]
if sharepointPath:
return await self._matchConnectionToPath(msftConnections, sharepointPath)
logger.info(f"Multiple Microsoft connections found, using first one: {msftConnections[0].get('id')}")
logger.info(f"Multiple Microsoft connections found, using first one: {msftConnections[0].id}")
return msftConnections[0]
except Exception:
logger.error('Error getting SharePoint connection')

View file

@ -165,13 +165,11 @@ def _syncTemplateRolesToDb() -> int:
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
rootInterface = getRootInterface()
db = rootInterface.db
existingRoles = db.getRecordset(
Role,
recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
)
existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
# Get existing template roles (Pydantic models)
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
templateRoles = [r for r in existingRoles if r.mandateId is None]
existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
@ -179,7 +177,7 @@ def _syncTemplateRolesToDb() -> int:
if roleLabel in existingRoleLabels:
roleId = existingRoleLabels[roleLabel]
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
else:
newRole = Role(
roleLabel=roleLabel,
@ -189,65 +187,65 @@ def _syncTemplateRolesToDb() -> int:
featureInstanceId=None,
isSystemRole=False
)
createdRole = db.recordCreate(Role, newRole.model_dump())
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
existingRoleLabels[roleLabel] = roleId
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logging.getLogger(__name__).info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
if createdCount > 0:
logging.getLogger(__name__).info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
_repairInstanceRolesAccessRules(db, existingRoleLabels)
_repairInstanceRolesAccessRules(rootInterface, existingRoleLabels)
return createdCount
except Exception as e:
logging.getLogger(__name__).error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
return 0
def _repairInstanceRolesAccessRules(db, templateRoleLabels: dict) -> int:
def _repairInstanceRolesAccessRules(rootInterface, templateRoleLabels: dict) -> int:
"""Repair instance-specific roles by copying AccessRules from their template roles."""
from modules.datamodels.datamodelRbac import Role, AccessRule
repairedCount = 0
allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
allRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
instanceRoles = [r for r in allRoles if r.mandateId is not None]
for instanceRole in instanceRoles:
roleLabel = instanceRole.get("roleLabel")
instanceRoleId = instanceRole.get("id")
roleLabel = instanceRole.roleLabel
instanceRoleId = str(instanceRole.id)
templateRoleId = templateRoleLabels.get(roleLabel)
if not templateRoleId:
continue
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
existingRules = rootInterface.getAccessRulesByRole(instanceRoleId)
if existingRules:
continue
templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
templateRules = rootInterface.getAccessRulesByRole(templateRoleId)
if not templateRules:
continue
for rule in templateRules:
newRule = AccessRule(
roleId=instanceRoleId,
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"),
context=rule.context,
item=rule.item,
view=rule.view if rule.view else False,
read=rule.read,
create=rule.create,
update=rule.update,
delete=rule.delete,
)
db.recordCreate(AccessRule, newRule.model_dump())
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
repairedCount += 1
return repairedCount
def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: list) -> int:
def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: list) -> int:
"""Ensure AccessRules exist for a role based on templates."""
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
existingSignatures = {(r.get("context"), r.get("item")) for r in existingRules}
existingRules = rootInterface.getAccessRulesByRole(roleId)
existingSignatures = {(str(r.context) if r.context else None, r.item) for r in existingRules}
createdCount = 0
for template in ruleTemplates or []:
@ -273,7 +271,7 @@ def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: list) -> int:
update=template.get("update"),
delete=template.get("delete"),
)
db.recordCreate(AccessRule, newRule.model_dump())
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
createdCount += 1
existingSignatures.add((context, item))
return createdCount

View file

@ -267,14 +267,11 @@ def _syncTemplateRolesToDb() -> int:
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
rootInterface = getRootInterface()
db = rootInterface.db
# Get existing template roles for this feature
existingRoles = db.getRecordset(
Role,
recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
)
existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
# Get existing template roles for this feature (Pydantic models)
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
templateRoles = [r for r in existingRoles if r.mandateId is None]
existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
@ -285,7 +282,7 @@ def _syncTemplateRolesToDb() -> int:
logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
# Ensure AccessRules exist for this role
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
else:
# Create new template role
newRole = Role(
@ -296,11 +293,11 @@ def _syncTemplateRolesToDb() -> int:
featureInstanceId=None,
isSystemRole=False
)
createdRole = db.recordCreate(Role, newRole.model_dump())
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
# Create AccessRules for this role
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
@ -309,7 +306,7 @@ def _syncTemplateRolesToDb() -> int:
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
# Repair instance-specific roles that are missing AccessRules
_repairInstanceRolesAccessRules(db, existingRoleLabels)
_repairInstanceRolesAccessRules(rootInterface, existingRoleLabels)
return createdCount
@ -318,13 +315,13 @@ def _syncTemplateRolesToDb() -> int:
return 0
def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> int:
def _repairInstanceRolesAccessRules(rootInterface, templateRoleLabels: Dict[str, str]) -> int:
"""
Repair instance-specific roles by copying AccessRules from their template roles.
This ensures instance roles created before AccessRules were defined get updated.
Args:
db: Database connector
rootInterface: Root interface instance
templateRoleLabels: Dict mapping roleLabel to template role ID
Returns:
@ -334,41 +331,41 @@ def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> i
repairedCount = 0
# Get all instance-specific roles for this feature (mandateId is NOT None)
allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
# Get all instance-specific roles for this feature (Pydantic models)
allRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
instanceRoles = [r for r in allRoles if r.mandateId is not None]
for instanceRole in instanceRoles:
roleLabel = instanceRole.get("roleLabel")
instanceRoleId = instanceRole.get("id")
roleLabel = instanceRole.roleLabel
instanceRoleId = str(instanceRole.id)
# Find matching template role
templateRoleId = templateRoleLabels.get(roleLabel)
if not templateRoleId:
continue
# Check if instance role has AccessRules
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
# Check if instance role has AccessRules (Pydantic models)
existingRules = rootInterface.getAccessRulesByRole(instanceRoleId)
if existingRules:
continue # Already has rules, skip
# Copy AccessRules from template role
templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
# Copy AccessRules from template role (Pydantic models)
templateRules = rootInterface.getAccessRulesByRole(templateRoleId)
if not templateRules:
continue # Template has no rules
for rule in templateRules:
newRule = AccessRule(
roleId=instanceRoleId,
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"),
context=rule.context,
item=rule.item,
view=rule.view if rule.view else False,
read=rule.read,
create=rule.create,
update=rule.update,
delete=rule.delete,
)
db.recordCreate(AccessRule, newRule.model_dump())
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
logger.info(f"Repaired instance role '{roleLabel}' (ID: {instanceRoleId}): copied {len(templateRules)} AccessRules from template")
repairedCount += 1
@ -379,12 +376,12 @@ def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> i
return repairedCount
def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
"""
Ensure AccessRules exist for a role based on templates.
Args:
db: Database connector
rootInterface: Root interface instance
roleId: Role ID
ruleTemplates: List of rule templates
@ -393,13 +390,13 @@ def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any
"""
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
# Get existing rules for this role
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
# Get existing rules for this role (Pydantic models)
existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
existingSignatures = set()
for rule in existingRules:
sig = (rule.get("context"), rule.get("item"))
sig = (str(rule.context) if rule.context else None, rule.item)
existingSignatures.add(sig)
createdCount = 0
@ -431,7 +428,7 @@ def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any
update=template.get("update"),
delete=template.get("delete"),
)
db.recordCreate(AccessRule, newRule.model_dump())
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
createdCount += 1
if createdCount > 0:

View file

@ -1363,17 +1363,11 @@ async def get_instance_roles(
rootInterface = getRootInterface()
# Get instance-specific roles (mandateId set, featureInstanceId matches)
roles = rootInterface.db.getRecordset(
Role,
recordFilter={
"featureCode": "trustee",
"featureInstanceId": instanceId
}
)
# Get instance-specific roles (Pydantic models)
roles = rootInterface.getRolesByFeatureCode("trustee", featureInstanceId=instanceId)
return PaginatedResponse(
items=roles,
items=[r.model_dump() for r in roles],
pagination=None
)
@ -1390,18 +1384,16 @@ async def get_instance_role(
mandateId = await _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface()
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
role = rootInterface.getRole(roleId)
if not roles:
if not role:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
role = roles[0]
# Verify role belongs to this instance
if role.get("featureInstanceId") != instanceId:
if str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
return role
return role.model_dump()
@router.get("/{instanceId}/instance-roles/{roleId}/rules", response_model=PaginatedResponse)
@ -1420,19 +1412,16 @@ async def get_instance_role_rules(
rootInterface = getRootInterface()
# Verify role belongs to this instance
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if not roles or roles[0].get("featureInstanceId") != instanceId:
# Verify role belongs to this instance (Pydantic model)
role = rootInterface.getRole(roleId)
if not role or str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
# Get AccessRules for this role
rules = rootInterface.db.getRecordset(
AccessRule,
recordFilter={"roleId": roleId}
)
# Get AccessRules for this role (Pydantic models)
rules = rootInterface.getAccessRulesByRole(roleId)
return PaginatedResponse(
items=rules,
items=[r.model_dump() for r in rules],
pagination=None
)
@ -1454,9 +1443,9 @@ async def create_instance_role_rule(
rootInterface = getRootInterface()
# Verify role belongs to this instance
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if not roles or roles[0].get("featureInstanceId") != instanceId:
# Verify role belongs to this instance (Pydantic model)
role = rootInterface.getRole(roleId)
if not role or str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
# Create the rule
@ -1505,14 +1494,14 @@ async def update_instance_role_rule(
rootInterface = getRootInterface()
# Verify role belongs to this instance
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if not roles or roles[0].get("featureInstanceId") != instanceId:
# Verify role belongs to this instance (Pydantic model)
role = rootInterface.getRole(roleId)
if not role or str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
# Verify rule belongs to role
existingRules = rootInterface.db.getRecordset(AccessRule, recordFilter={"id": ruleId})
if not existingRules or existingRules[0].get("roleId") != roleId:
# Verify rule belongs to role (Pydantic model)
existingRule = rootInterface.getAccessRule(ruleId)
if not existingRule or str(existingRule.roleId) != roleId:
raise HTTPException(status_code=404, detail=f"Rule {ruleId} not found for this role")
# Update only allowed fields
@ -1529,7 +1518,7 @@ async def update_instance_role_rule(
updateData["delete"] = ruleData["delete"]
if not updateData:
return existingRules[0]
return existingRule.model_dump()
try:
updated = rootInterface.db.recordModify(AccessRule, ruleId, updateData)
@ -1556,14 +1545,14 @@ async def delete_instance_role_rule(
rootInterface = getRootInterface()
# Verify role belongs to this instance
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if not roles or roles[0].get("featureInstanceId") != instanceId:
# Verify role belongs to this instance (Pydantic model)
role = rootInterface.getRole(roleId)
if not role or str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
# Verify rule belongs to role
existingRules = rootInterface.db.getRecordset(AccessRule, recordFilter={"id": ruleId})
if not existingRules or existingRules[0].get("roleId") != roleId:
# Verify rule belongs to role (Pydantic model)
existingRule = rootInterface.getAccessRule(ruleId)
if not existingRule or str(existingRule.roleId) != roleId:
raise HTTPException(status_code=404, detail=f"Rule {ruleId} not found for this role")
try:

View file

@ -97,7 +97,7 @@ class AiObjects:
return AiCallResponse(
content=errorMsg,
modelName="error",
priceUsd=0.0,
priceCHF=0.0,
processingTime=0.0,
bytesSent=0,
bytesReceived=0,
@ -135,7 +135,7 @@ class AiObjects:
return AiCallResponse(
content=errorMsg,
modelName="error",
priceUsd=0.0,
priceCHF=0.0,
processingTime=0.0,
bytesSent=0,
bytesReceived=0,
@ -147,7 +147,7 @@ class AiObjects:
return AiCallResponse(
content=errorMsg,
modelName="error",
priceUsd=0.0,
priceCHF=0.0,
processingTime=0.0,
bytesSent=inputBytes,
bytesReceived=outputBytes,
@ -213,12 +213,12 @@ class AiObjects:
outputBytes = len(content.encode("utf-8"))
# Calculate price using model's own price calculation method
priceUsd = model.calculatePriceUsd(processingTime, inputBytes, outputBytes)
priceCHF = model.calculatepriceCHF(processingTime, inputBytes, outputBytes)
return AiCallResponse(
content=content,
modelName=model.name,
priceUsd=priceUsd,
priceCHF=priceCHF,
processingTime=processingTime,
bytesSent=inputBytes,
bytesReceived=outputBytes,

View file

@ -73,6 +73,10 @@ def initBootstrap(db: DatabaseConnector) -> None:
# Seed automation templates (after admin user exists)
initAutomationTemplates(db, adminUserId)
# Initialize feature instances for root mandate
if mandateId:
initRootMandateFeatures(db, mandateId)
def initAutomationTemplates(dbApp: DatabaseConnector, adminUserId: Optional[str] = None) -> None:
"""
@ -153,6 +157,67 @@ def initAutomationTemplates(dbApp: DatabaseConnector, adminUserId: Optional[str]
logger.info("System bootstrap completed")
def initRootMandateFeatures(db: DatabaseConnector, mandateId: str) -> None:
"""
Create feature instances for root mandate (chatplayground, automation).
These features are available to all users by default.
Args:
db: Database connector instance
mandateId: Root mandate ID
"""
from modules.datamodels.datamodelFeatures import FeatureInstance
from modules.interfaces.interfaceFeatures import getFeatureInterface
logger.info("Initializing root mandate features")
# Features to create instances for
featuresToCreate = [
{"code": "chatplayground", "label": "Chat Playground"},
{"code": "automation", "label": "Automation"},
]
featureInterface = getFeatureInterface(db)
for featureConfig in featuresToCreate:
featureCode = featureConfig["code"]
featureLabel = featureConfig["label"]
try:
# Check if instance already exists
existingInstances = db.getRecordset(
FeatureInstance,
recordFilter={
"mandateId": mandateId,
"featureCode": featureCode
}
)
if existingInstances:
logger.info(f"Feature instance for '{featureCode}' already exists in root mandate")
continue
# Create feature instance with template roles copied
instance = featureInterface.createFeatureInstance(
featureCode=featureCode,
mandateId=mandateId,
label=featureLabel,
enabled=True,
copyTemplateRoles=True
)
if instance:
instanceId = instance.get("id") if isinstance(instance, dict) else instance.id
logger.info(f"Created feature instance '{instanceId}' for '{featureCode}' in root mandate")
else:
logger.warning(f"Failed to create feature instance for '{featureCode}'")
except Exception as e:
logger.error(f"Error creating feature instance for '{featureCode}': {e}")
logger.info("Root mandate features initialization completed")
def initRootMandate(db: DatabaseConnector) -> Optional[str]:
"""
Creates the Root mandate if it doesn't exist.

View file

@ -45,6 +45,7 @@ from modules.datamodels.datamodelMembership import (
)
from modules.datamodels.datamodelFeatures import Feature, FeatureInstance
from modules.datamodels.datamodelInvitation import Invitation
from modules.datamodels.datamodelNotification import UserNotification
logger = logging.getLogger(__name__)
@ -733,6 +734,9 @@ class AppObjects:
# Clear cache to ensure fresh data (already done above)
# Grant access to root mandate features (chatplayground, automation)
self._grantRootMandateFeatureAccess(createdUser[0]["id"])
return User(**createdUser[0])
except ValueError as e:
@ -796,6 +800,99 @@ class AppObjects:
logger.error(f"Error updating user: {str(e)}")
raise ValueError(f"Failed to update user: {str(e)}")
def _grantRootMandateFeatureAccess(self, userId: str) -> None:
"""
Grant a new user access to root mandate features (chatplayground, automation).
Creates FeatureAccess with viewer role for each feature instance.
Args:
userId: User ID to grant access to
"""
try:
from modules.datamodels.datamodelFeatures import FeatureInstance
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
from modules.datamodels.datamodelRbac import Role
# Get root mandate ID (first mandate in system)
allMandates = self.db.getRecordset(Mandate)
if not allMandates:
logger.debug("No mandates found, skipping feature access grant")
return
rootMandateId = allMandates[0].get("id")
# Feature codes to grant access to
rootFeatureCodes = ["chatplayground", "automation"]
# Get feature instances for root mandate
allInstances = self.db.getRecordset(FeatureInstance)
featureInstances = [
inst for inst in allInstances
if inst.get("mandateId") == rootMandateId
and inst.get("featureCode") in rootFeatureCodes
and inst.get("enabled") == True
]
if not featureInstances:
logger.debug("No root mandate feature instances found, skipping feature access grant")
return
# Grant access to each feature instance
for instance in featureInstances:
instanceId = instance.get("id")
featureCode = instance.get("featureCode")
# Check if user already has access
existingAccess = self.db.getRecordset(
FeatureAccess,
recordFilter={
"userId": userId,
"featureInstanceId": instanceId
}
)
if existingAccess:
logger.debug(f"User {userId} already has access to feature instance {instanceId}")
continue
# Create FeatureAccess
featureAccess = FeatureAccess(
userId=userId,
featureInstanceId=instanceId,
enabled=True
)
createdAccess = self.db.recordCreate(FeatureAccess, featureAccess.model_dump())
if not createdAccess:
logger.warning(f"Failed to create FeatureAccess for user {userId} to instance {instanceId}")
continue
featureAccessId = createdAccess.get("id")
# Get viewer role for this feature instance
allRoles = self.db.getRecordset(Role)
viewerRoles = [
r for r in allRoles
if r.get("featureInstanceId") == instanceId
and r.get("roleLabel") == "viewer"
]
if viewerRoles:
# Create FeatureAccessRole junction
featureAccessRole = FeatureAccessRole(
featureAccessId=featureAccessId,
roleId=viewerRoles[0].get("id")
)
self.db.recordCreate(FeatureAccessRole, featureAccessRole.model_dump())
logger.debug(f"Granted viewer role for {featureCode} to user {userId}")
else:
logger.warning(f"No viewer role found for feature instance {instanceId} ({featureCode})")
logger.info(f"Granted root mandate feature access to user {userId}")
except Exception as e:
# Log but don't fail user creation
logger.error(f"Error granting root mandate feature access to user {userId}: {e}")
def disableUser(self, userId: str) -> User:
"""Disables a user if current user has permission."""
return self.updateUser(userId, {"enabled": False})
@ -1209,6 +1306,31 @@ class AppObjects:
logger.error(f"Error getting user connections: {str(e)}")
return []
def getUserConnectionById(self, connectionId: str) -> Optional[UserConnection]:
"""Get a single UserConnection by ID."""
try:
connections = self.db.getRecordset(
UserConnection, recordFilter={"id": connectionId}
)
if connections:
conn_dict = connections[0]
return UserConnection(
id=conn_dict["id"],
userId=conn_dict["userId"],
authority=conn_dict.get("authority"),
externalId=conn_dict.get("externalId", ""),
externalUsername=conn_dict.get("externalUsername", ""),
externalEmail=conn_dict.get("externalEmail"),
status=conn_dict.get("status", "pending"),
connectedAt=conn_dict.get("connectedAt"),
lastChecked=conn_dict.get("lastChecked"),
expiresAt=conn_dict.get("expiresAt"),
)
return None
except Exception as e:
logger.error(f"Error getting user connection by ID: {str(e)}")
return None
def addUserConnection(
self,
userId: str,
@ -1547,6 +1669,106 @@ class AppObjects:
logger.error(f"Error deleting UserMandate: {e}")
raise ValueError(f"Failed to delete UserMandate: {e}")
def getUserMandatesByMandate(self, mandateId: str) -> List[UserMandate]:
"""
Get all UserMandate records for a specific mandate.
Args:
mandateId: Mandate ID
Returns:
List of UserMandate objects
"""
try:
records = self.db.getRecordset(
UserMandate,
recordFilter={"mandateId": mandateId}
)
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(UserMandate(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting UserMandates for mandate {mandateId}: {e}")
return []
def getUserMandateRoles(self, userMandateId: str) -> List[UserMandateRole]:
"""
Get all UserMandateRole records for a UserMandate.
Args:
userMandateId: UserMandate ID
Returns:
List of UserMandateRole objects
"""
try:
records = self.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": userMandateId}
)
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(UserMandateRole(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting UserMandateRoles: {e}")
return []
def deleteUserMandateRoles(self, userMandateId: str) -> int:
"""
Delete all role assignments for a UserMandate.
Args:
userMandateId: UserMandate ID
Returns:
Number of deleted role assignments
"""
try:
records = self.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": userMandateId}
)
deletedCount = 0
for record in records:
if self.db.recordDelete(UserMandateRole, record.get("id")):
deletedCount += 1
return deletedCount
except Exception as e:
logger.error(f"Error deleting UserMandateRoles: {e}")
return 0
def validateRoleForMandate(self, roleId: str, mandateId: str) -> Role:
"""
Validate a role exists and belongs to the specified mandate (or is global).
Args:
roleId: Role ID to validate
mandateId: Mandate ID for context validation
Returns:
Role object if valid
Raises:
ValueError: If role not found or belongs to different mandate
"""
role = self.getRole(roleId)
if not role:
raise ValueError(f"Role {roleId} not found")
# Check mandate scope
if role.mandateId and str(role.mandateId) != str(mandateId):
raise ValueError(f"Role {roleId} belongs to a different mandate")
# Check feature-instance scope (not allowed at mandate level)
if role.featureInstanceId:
raise ValueError(f"Role {roleId} is a feature-instance role and cannot be assigned at mandate level")
return role
def getRoleIdsForUserMandate(self, userMandateId: str) -> List[str]:
"""
Get all role IDs assigned to a UserMandate.
@ -1688,6 +1910,30 @@ class AppObjects:
logger.error(f"Error getting FeatureAccesses: {e}")
return []
def getFeatureAccessesByInstance(self, featureInstanceId: str) -> List[FeatureAccess]:
"""
Get all FeatureAccess records for a specific feature instance.
Args:
featureInstanceId: FeatureInstance ID
Returns:
List of FeatureAccess objects
"""
try:
records = self.db.getRecordset(
FeatureAccess,
recordFilter={"featureInstanceId": featureInstanceId}
)
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(FeatureAccess(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting FeatureAccesses for instance {featureInstanceId}: {e}")
return []
def createFeatureAccess(self, userId: str, featureInstanceId: str, roleIds: List[str] = None) -> FeatureAccess:
"""
Create a FeatureAccess record (grant user access to feature instance).
@ -1750,6 +1996,445 @@ class AppObjects:
logger.error(f"Error getting role IDs for FeatureAccess: {e}")
return []
def deleteFeatureAccessRoles(self, featureAccessId: str) -> int:
"""
Delete all FeatureAccessRole records for a FeatureAccess.
Args:
featureAccessId: FeatureAccess ID
Returns:
Number of records deleted
"""
try:
records = self.db.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
count = 0
for record in records:
recordId = record.get("id")
if recordId:
self.db.recordDelete(FeatureAccessRole, recordId)
count += 1
return count
except Exception as e:
logger.error(f"Error deleting FeatureAccessRoles for {featureAccessId}: {e}")
return 0
# ============================================
# Invitation Methods
# ============================================
def getInvitation(self, invitationId: str) -> Optional[Invitation]:
"""
Get an invitation by ID.
Args:
invitationId: Invitation ID
Returns:
Invitation object if found, None otherwise
"""
try:
records = self.db.getRecordset(Invitation, recordFilter={"id": invitationId})
if records:
cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
return Invitation(**cleanedRecord)
return None
except Exception as e:
logger.error(f"Error getting invitation {invitationId}: {e}")
return None
def getInvitationByToken(self, token: str) -> Optional[Invitation]:
"""
Get an invitation by token.
Args:
token: Invitation token
Returns:
Invitation object if found, None otherwise
"""
try:
records = self.db.getRecordset(Invitation, recordFilter={"token": token})
if records:
cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
return Invitation(**cleanedRecord)
return None
except Exception as e:
logger.error(f"Error getting invitation by token: {e}")
return None
def getInvitationsByMandate(self, mandateId: str) -> List[Invitation]:
"""
Get all invitations for a mandate.
Args:
mandateId: Mandate ID
Returns:
List of Invitation objects
"""
try:
records = self.db.getRecordset(Invitation, recordFilter={"mandateId": mandateId})
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(Invitation(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting invitations for mandate {mandateId}: {e}")
return []
def getInvitationsByCreator(self, creatorId: str) -> List[Invitation]:
"""
Get all invitations created by a user.
Args:
creatorId: User ID who created the invitations
Returns:
List of Invitation objects
"""
try:
records = self.db.getRecordset(Invitation, recordFilter={"createdBy": creatorId})
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(Invitation(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting invitations by creator {creatorId}: {e}")
return []
def getInvitationsByUsedBy(self, usedById: str) -> List[Invitation]:
"""
Get all invitations used by a user.
Args:
usedById: User ID who used the invitations
Returns:
List of Invitation objects
"""
try:
records = self.db.getRecordset(Invitation, recordFilter={"usedBy": usedById})
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(Invitation(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting invitations used by {usedById}: {e}")
return []
def getInvitationsByTargetUsername(self, targetUsername: str) -> List[Invitation]:
"""
Get all invitations for a target username.
Args:
targetUsername: Target username for the invitations
Returns:
List of Invitation objects
"""
try:
records = self.db.getRecordset(Invitation, recordFilter={"targetUsername": targetUsername})
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(Invitation(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting invitations for target username {targetUsername}: {e}")
return []
# ============================================
# Additional Helper Methods
# ============================================
def getAllUsers(self) -> List[User]:
"""
Get all users (for SysAdmin only).
Returns:
List of User objects (without sensitive fields)
"""
try:
records = self.db.getRecordset(UserInDB)
result = []
for record in records:
# Filter out sensitive and internal fields
cleanedRecord = {
k: v for k, v in record.items()
if not k.startswith("_") and k not in ["hashedPassword", "resetToken", "resetTokenExpires"]
}
# Ensure roleLabels is a list
if cleanedRecord.get("roleLabels") is None:
cleanedRecord["roleLabels"] = []
result.append(User(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting all users: {e}")
return []
def getUserMandateById(self, userMandateId: str) -> Optional[UserMandate]:
"""
Get a UserMandate by its ID.
Args:
userMandateId: UserMandate ID
Returns:
UserMandate object if found, None otherwise
"""
try:
records = self.db.getRecordset(UserMandate, recordFilter={"id": userMandateId})
if records:
cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
return UserMandate(**cleanedRecord)
return None
except Exception as e:
logger.error(f"Error getting UserMandate {userMandateId}: {e}")
return None
def getUserMandateRolesByRole(self, roleId: str) -> List[UserMandateRole]:
"""
Get all UserMandateRole records for a specific role.
Args:
roleId: Role ID
Returns:
List of UserMandateRole objects
"""
try:
records = self.db.getRecordset(UserMandateRole, recordFilter={"roleId": roleId})
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(UserMandateRole(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting UserMandateRoles for role {roleId}: {e}")
return []
def getFeatureInstance(self, instanceId: str):
"""
Get a FeatureInstance by ID.
Args:
instanceId: FeatureInstance ID
Returns:
FeatureInstance object if found, None otherwise
"""
try:
records = self.db.getRecordset(FeatureInstance, recordFilter={"id": instanceId})
if records:
cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
return FeatureInstance(**cleanedRecord)
return None
except Exception as e:
logger.error(f"Error getting FeatureInstance {instanceId}: {e}")
return None
def getFeatureByCode(self, featureCode: str) -> Optional[Feature]:
"""
Get a Feature by its code.
Args:
featureCode: Feature code
Returns:
Feature object if found, None otherwise
"""
try:
records = self.db.getRecordset(Feature, recordFilter={"code": featureCode})
if records:
cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
return Feature(**cleanedRecord)
return None
except Exception as e:
logger.error(f"Error getting Feature by code {featureCode}: {e}")
return None
def getFeatureInstancesByMandate(self, mandateId: str, enabledOnly: bool = False) -> List[FeatureInstance]:
"""
Get all FeatureInstances for a mandate.
Args:
mandateId: Mandate ID
enabledOnly: If True, only return enabled instances
Returns:
List of FeatureInstance objects
"""
try:
recordFilter = {"mandateId": mandateId}
if enabledOnly:
recordFilter["enabled"] = True
records = self.db.getRecordset(FeatureInstance, recordFilter=recordFilter)
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(FeatureInstance(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting FeatureInstances for mandate {mandateId}: {e}")
return []
# ============================================
# Notification Methods
# ============================================
def getNotification(self, notificationId: str) -> Optional[UserNotification]:
"""
Get a notification by ID.
Args:
notificationId: Notification ID
Returns:
UserNotification object if found, None otherwise
"""
try:
records = self.db.getRecordset(UserNotification, recordFilter={"id": notificationId})
if records:
cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
return UserNotification(**cleanedRecord)
return None
except Exception as e:
logger.error(f"Error getting notification {notificationId}: {e}")
return None
def getNotificationsByUser(
self,
userId: str,
status: Optional[str] = None,
limit: Optional[int] = None
) -> List[UserNotification]:
"""
Get notifications for a user.
Args:
userId: User ID
status: Optional status filter (e.g., 'unread')
limit: Optional limit on number of results
Returns:
List of UserNotification objects
"""
try:
recordFilter = {"userId": userId}
if status:
recordFilter["status"] = status
records = self.db.getRecordset(UserNotification, recordFilter=recordFilter)
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(UserNotification(**cleanedRecord))
# Sort by createdAt descending
result.sort(key=lambda x: x.createdAt or 0, reverse=True)
if limit:
result = result[:limit]
return result
except Exception as e:
logger.error(f"Error getting notifications for user {userId}: {e}")
return []
# ============================================
# AccessRule Methods
# ============================================
def getAccessRule(self, ruleId: str) -> Optional[AccessRule]:
"""
Get an AccessRule by ID.
Args:
ruleId: AccessRule ID
Returns:
AccessRule object if found, None otherwise
"""
try:
records = self.db.getRecordset(AccessRule, recordFilter={"id": ruleId})
if records:
cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
return AccessRule(**cleanedRecord)
return None
except Exception as e:
logger.error(f"Error getting AccessRule {ruleId}: {e}")
return None
def getAccessRulesByRole(self, roleId: str) -> List[AccessRule]:
"""
Get all AccessRules for a role.
Args:
roleId: Role ID
Returns:
List of AccessRule objects
"""
try:
records = self.db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(AccessRule(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting AccessRules for role {roleId}: {e}")
return []
def getRolesByFeatureInstance(self, featureInstanceId: str) -> List[Role]:
"""
Get all roles for a feature instance.
Args:
featureInstanceId: FeatureInstance ID
Returns:
List of Role objects
"""
try:
records = self.db.getRecordset(Role, recordFilter={"featureInstanceId": featureInstanceId})
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(Role(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting roles for feature instance {featureInstanceId}: {e}")
return []
def getRolesByFeatureCode(self, featureCode: str, featureInstanceId: Optional[str] = None) -> List[Role]:
"""
Get all roles for a feature code, optionally filtered by instance.
Args:
featureCode: Feature code
featureInstanceId: Optional FeatureInstance ID filter
Returns:
List of Role objects
"""
try:
recordFilter = {"featureCode": featureCode}
if featureInstanceId:
recordFilter["featureInstanceId"] = featureInstanceId
records = self.db.getRecordset(Role, recordFilter=recordFilter)
result = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
result.append(Role(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting roles for feature code {featureCode}: {e}")
return []
# Token methods
def saveAccessToken(self, token: Token, replace_existing: bool = True) -> None:
@ -1908,6 +2593,56 @@ class AppObjects:
)
return None
def getTokensByConnectionIdAndAuthority(
self, connectionId: str, authority: AuthAuthority
) -> List[Token]:
"""Get tokens for a connection with specific authority."""
try:
tokens = self.db.getRecordset(
Token, recordFilter={
"connectionId": connectionId,
"authority": authority.value if hasattr(authority, 'value') else str(authority)
}
)
result = []
for token_dict in tokens:
cleanedRecord = {k: v for k, v in token_dict.items() if not k.startswith("_")}
result.append(Token(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting tokens by connection and authority: {str(e)}")
return []
def getTokensByUserIdNoConnection(
self, userId: str, authority: AuthAuthority
) -> List[Token]:
"""Get tokens for a user without a connection (access tokens)."""
try:
tokens = self.db.getRecordset(
Token, recordFilter={
"userId": userId,
"connectionId": None,
"authority": authority.value if hasattr(authority, 'value') else str(authority)
}
)
result = []
for token_dict in tokens:
cleanedRecord = {k: v for k, v in token_dict.items() if not k.startswith("_")}
result.append(Token(**cleanedRecord))
return result
except Exception as e:
logger.error(f"Error getting tokens by user and authority: {str(e)}")
return []
def getAllTokens(self, recordFilter: dict = None) -> List[dict]:
"""Get all tokens with optional filtering (returns raw dicts)."""
try:
tokens = self.db.getRecordset(Token, recordFilter=recordFilter or {})
return tokens
except Exception as e:
logger.error(f"Error getting all tokens: {str(e)}")
return []
def findActiveTokenById(
self,
tokenId: str,
@ -2340,6 +3075,42 @@ class AppObjects:
logger.error(f"Error getting role by label {roleLabel}: {str(e)}")
return None
def getRoleByLabelAndScope(
self,
roleLabel: str,
mandateId: Optional[str] = None,
featureInstanceId: Optional[str] = None,
featureCode: Optional[str] = None
) -> Optional[Role]:
"""
Get a role by label with scope filtering.
Args:
roleLabel: Role label
mandateId: Mandate ID (use None for global roles)
featureInstanceId: Feature instance ID
featureCode: Feature code
Returns:
Role object if found, None otherwise
"""
try:
recordFilter = {"roleLabel": roleLabel}
if mandateId is not None:
recordFilter["mandateId"] = mandateId
if featureInstanceId is not None:
recordFilter["featureInstanceId"] = featureInstanceId
if featureCode is not None:
recordFilter["featureCode"] = featureCode
roles = self.db.getRecordset(Role, recordFilter=recordFilter)
if roles:
return Role(**roles[0])
return None
except Exception as e:
logger.error(f"Error getting role by label and scope {roleLabel}: {str(e)}")
return None
def getAllRoles(self, pagination: Optional[PaginationParams] = None) -> Union[List[Role], PaginatedResult]:
"""
Get all roles with optional pagination, sorting, and filtering.

View file

@ -204,38 +204,26 @@ async def get_my_feature_instances(
def _getUserRolesInInstance(rootInterface, userId: str, instanceId: str) -> List[str]:
"""Get all role labels for a user in a feature instance."""
try:
from modules.datamodels.datamodelRbac import Role
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
# Get FeatureAccess for this user and instance (Pydantic model)
featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
# Get FeatureAccess for this user and instance
featureAccesses = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
if featureAccess:
# Get role IDs via interface method
roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
if featureAccesses:
featureAccessId = featureAccesses[0].get("id")
# Get role IDs via FeatureAccessRole junction table
featureAccessRoles = rootInterface.db.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
if featureAccessRoles:
# Get ALL roles, not just the first one
if roleIds:
# Get ALL roles and extract labels
roleLabels = []
for far in featureAccessRoles:
roleId = far.get("roleId")
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roles:
roleLabels.append(roles[0].get("roleLabel", "user"))
for roleId in roleIds:
role = rootInterface.getRole(roleId)
if role:
roleLabels.append(role.roleLabel)
return roleLabels if roleLabels else ["user"]
return ["user"] # Default
return ["user"] # Default - no access means basic user level
except Exception as e:
logger.debug(f"Error getting user roles: {e}")
return ["user"]
return ["user"] # Fail-safe: default to basic user
def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict[str, Any]:
@ -249,66 +237,53 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
}
try:
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext, Role
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
from modules.datamodels.datamodelRbac import AccessRuleContext
# Get FeatureAccess for this user and instance
featureAccesses = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
# Get FeatureAccess for this user and instance (Pydantic model)
featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
logger.debug(f"_getInstancePermissions: userId={userId}, instanceId={instanceId}, featureAccesses={len(featureAccesses) if featureAccesses else 0}")
logger.debug(f"_getInstancePermissions: userId={userId}, instanceId={instanceId}, featureAccess={featureAccess is not None}")
if not featureAccesses:
if not featureAccess:
logger.debug(f"_getInstancePermissions: No FeatureAccess found for user {userId} and instance {instanceId}")
return permissions
# Get role IDs via FeatureAccessRole junction table
featureAccessId = featureAccesses[0].get("id")
featureAccessRoles = rootInterface.db.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
roleIds = [far.get("roleId") for far in featureAccessRoles]
# Get role IDs via interface method
roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
logger.debug(f"_getInstancePermissions: featureAccessId={featureAccessId}, roleIds={roleIds}")
logger.debug(f"_getInstancePermissions: featureAccessId={featureAccess.id}, roleIds={roleIds}")
if not roleIds:
logger.debug(f"_getInstancePermissions: No roles found for FeatureAccess {featureAccessId}")
logger.debug(f"_getInstancePermissions: No roles found for FeatureAccess {featureAccess.id}")
return permissions
# Check if user has admin role
for roleId in roleIds:
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roles:
roleLabel = roles[0].get("roleLabel", "").lower()
if "admin" in roleLabel:
role = rootInterface.getRole(roleId)
if role and "admin" in role.roleLabel.lower():
permissions["isAdmin"] = True
break
# Get permissions (AccessRules) for all roles
for roleId in roleIds:
accessRules = rootInterface.db.getRecordset(
AccessRule,
recordFilter={"roleId": roleId}
)
# Get all rules for this role (returns Pydantic models)
accessRules = rootInterface.getAccessRules(roleId=roleId)
logger.debug(f"_getInstancePermissions: roleId={roleId}, accessRules={len(accessRules) if accessRules else 0}")
for rule in accessRules:
context = rule.get("context", "")
item = rule.get("item", "")
context = rule.context
item = rule.item or ""
# Handle DATA context (tables/fields)
if context == "DATA" or context == AccessRuleContext.DATA:
if context == AccessRuleContext.DATA or context == "DATA":
if item:
# Check if it's a field (table.field) or table
if "." in item:
tableName, fieldName = item.split(".", 1)
if fieldName not in permissions["fields"]:
permissions["fields"][fieldName] = {"view": False}
permissions["fields"][fieldName]["view"] = permissions["fields"][fieldName]["view"] or rule.get("view", False)
permissions["fields"][fieldName]["view"] = permissions["fields"][fieldName]["view"] or rule.view
else:
tableName = item
if tableName not in permissions["tables"]:
@ -322,20 +297,18 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
# Merge permissions (highest wins)
current = permissions["tables"][tableName]
current["view"] = current["view"] or rule.get("view", False)
current["read"] = _mergeAccessLevel(current["read"], rule.get("read") or "n")
current["create"] = _mergeAccessLevel(current["create"], rule.get("create") or "n")
current["update"] = _mergeAccessLevel(current["update"], rule.get("update") or "n")
current["delete"] = _mergeAccessLevel(current["delete"], rule.get("delete") or "n")
current["view"] = current["view"] or rule.view
current["read"] = _mergeAccessLevel(current["read"], rule.read or "n")
current["create"] = _mergeAccessLevel(current["create"], rule.create or "n")
current["update"] = _mergeAccessLevel(current["update"], rule.update or "n")
current["delete"] = _mergeAccessLevel(current["delete"], rule.delete or "n")
# Handle UI context (views)
# Views are stored with full objectKey (e.g., ui.feature.trustee.dashboard)
elif context == "UI" or context == AccessRuleContext.UI:
ruleView = rule.get("view", False)
elif context == AccessRuleContext.UI or context == "UI":
if item:
# Store with full objectKey as per Navigation-API-Konzept
permissions["views"][item] = permissions["views"].get(item, False) or ruleView
elif ruleView:
permissions["views"][item] = permissions["views"].get(item, False) or rule.view
elif rule.view:
# item=None means all views - set a wildcard flag
permissions["views"]["_all"] = True
@ -343,7 +316,7 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
except Exception as e:
logger.debug(f"Error getting instance permissions: {e}")
return permissions
return permissions # Fail-safe: no permissions on error
def _mergeAccessLevel(current: str, new: str) -> str:
@ -924,49 +897,35 @@ async def list_feature_instance_users(
detail="Access denied to this feature instance"
)
# Get all FeatureAccess records for this instance
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
from modules.datamodels.datamodelRbac import Role
featureAccesses = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"featureInstanceId": instanceId}
)
# Get all FeatureAccess records for this instance (Pydantic models)
featureAccesses = rootInterface.getFeatureAccessesByInstance(instanceId)
result = []
for fa in featureAccesses:
userId = fa.get("userId")
featureAccessId = fa.get("id")
# Get user info
users = rootInterface.db.getRecordset(UserInDB, recordFilter={"id": userId})
if not users:
# Get user info (Pydantic model)
user = rootInterface.getUser(str(fa.userId))
if not user:
continue
user = users[0]
# Get role IDs via FeatureAccessRole junction table
featureAccessRoles = rootInterface.db.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
roleIds = [far.get("roleId") for far in featureAccessRoles]
# Get role IDs via interface method
roleIds = rootInterface.getRoleIdsForFeatureAccess(str(fa.id))
# Get role labels
roleLabels = []
for roleId in roleIds:
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roles:
roleLabels.append(roles[0].get("roleLabel", ""))
role = rootInterface.getRole(roleId)
if role:
roleLabels.append(role.roleLabel)
result.append(FeatureInstanceUserResponse(
id=featureAccessId, # FeatureAccess ID as primary key
userId=userId,
username=user.get("username", ""),
email=user.get("email"),
fullName=user.get("fullName"),
id=str(fa.id), # FeatureAccess ID as primary key
userId=str(fa.userId),
username=user.username,
email=user.email,
fullName=user.fullName,
roleIds=roleIds,
roleLabels=roleLabels,
enabled=fa.get("enabled", True)
enabled=fa.enabled
))
return result
@ -1026,8 +985,8 @@ async def add_user_to_feature_instance(
)
# Verify user exists
users = rootInterface.db.getRecordset(UserInDB, recordFilter={"id": data.userId})
if not users:
user = rootInterface.getUser(data.userId)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User '{data.userId}' not found"
@ -1035,10 +994,7 @@ async def add_user_to_feature_instance(
# Check if user already has access
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
existingAccess = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"userId": data.userId, "featureInstanceId": instanceId}
)
existingAccess = rootInterface.getFeatureAccess(data.userId, instanceId)
if existingAccess:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
@ -1131,17 +1087,14 @@ async def remove_user_from_feature_instance(
# Find FeatureAccess record
from modules.datamodels.datamodelMembership import FeatureAccess
existingAccess = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
existingAccess = rootInterface.getFeatureAccess(userId, instanceId)
if not existingAccess:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not have access to this feature instance"
)
featureAccessId = existingAccess[0].get("id")
featureAccessId = str(existingAccess.id)
# Delete FeatureAccess (CASCADE will delete FeatureAccessRole records)
rootInterface.db.recordDelete(FeatureAccess, featureAccessId)
@ -1215,29 +1168,21 @@ async def update_feature_instance_user_roles(
# Find FeatureAccess record
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
existingAccess = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
existingAccess = rootInterface.getFeatureAccess(userId, instanceId)
if not existingAccess:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not have access to this feature instance"
)
featureAccessId = existingAccess[0].get("id")
featureAccessId = str(existingAccess.id)
# Update enabled flag if provided
if data.enabled is not None:
rootInterface.db.recordModify(FeatureAccess, featureAccessId, {"enabled": data.enabled})
# Delete existing FeatureAccessRole records
existingRoles = rootInterface.db.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
for role in existingRoles:
rootInterface.db.recordDelete(FeatureAccessRole, role.get("id"))
# Delete existing FeatureAccessRole records via interface method
rootInterface.deleteFeatureAccessRoles(featureAccessId)
# Create new FeatureAccessRole records
for roleId in data.roleIds:
@ -1304,21 +1249,17 @@ async def get_feature_instance_available_roles(
detail="Access denied to this feature instance"
)
# Get roles for this instance
from modules.datamodels.datamodelRbac import Role
instanceRoles = rootInterface.db.getRecordset(
Role,
recordFilter={"featureInstanceId": instanceId}
)
# Get roles for this instance using interface method
instanceRoles = rootInterface.getRolesByFeatureInstance(instanceId)
result = []
for role in instanceRoles:
result.append({
"id": role.get("id"),
"roleLabel": role.get("roleLabel"),
"description": role.get("description", {}),
"featureCode": role.get("featureCode"),
"isSystemRole": role.get("isSystemRole", False)
"id": role.id,
"roleLabel": role.roleLabel,
"description": role.description or {},
"featureCode": role.featureCode,
"isSystemRole": role.isSystemRole
})
return result
@ -1394,15 +1335,13 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
# Check if any of the user's roles is an admin role
try:
rootInterface = getRootInterface()
from modules.datamodels.datamodelRbac import Role
for roleId in context.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
roleLabel = role.get("roleLabel", "")
role = rootInterface.getRole(roleId)
if role:
roleLabel = role.roleLabel
# Admin role at mandate level (not feature-instance level)
if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
if roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
return True
return False

View file

@ -85,34 +85,31 @@ async def export_global_rbac(
try:
rootInterface = getRootInterface()
# Get all global template roles (mandateId is NULL)
allRoles = rootInterface.db.getRecordset(Role)
globalRoles = [r for r in allRoles if r.get("mandateId") is None]
# Get all global template roles (mandateId is NULL) using interface method
allRoles = rootInterface.getAllRoles()
globalRoles = [r for r in allRoles if r.mandateId is None]
exportRoles = []
for role in globalRoles:
roleId = role.get("id")
roleId = role.id
# Get access rules for this role
accessRules = rootInterface.db.getRecordset(
AccessRule,
recordFilter={"roleId": roleId}
)
# Get access rules for this role using interface method
accessRules = rootInterface.getAccessRulesByRole(roleId)
exportRoles.append(RoleExport(
roleLabel=role.get("roleLabel"),
description=role.get("description", {}),
featureCode=role.get("featureCode"),
isSystemRole=role.get("isSystemRole", False),
roleLabel=role.roleLabel,
description=role.description or {},
featureCode=role.featureCode,
isSystemRole=role.isSystemRole,
accessRules=[
{
"context": r.get("context"),
"item": r.get("item"),
"view": r.get("view", False),
"read": r.get("read"),
"create": r.get("create"),
"update": r.get("update"),
"delete": r.get("delete")
"context": r.context,
"item": r.item,
"view": r.view if r.view is not None else False,
"read": r.read,
"create": r.create,
"update": r.update,
"delete": r.delete
}
for r in accessRules
]
@ -191,21 +188,20 @@ async def import_global_rbac(
result.rolesSkipped += 1
continue
# Check if role exists (global role with same label and featureCode)
existingRoles = rootInterface.db.getRecordset(
Role,
recordFilter={
"roleLabel": roleLabel,
"mandateId": None,
"featureCode": featureCode
}
)
# Check if role exists (global role with same label and featureCode) using interface method
allRoles = rootInterface.getAllRoles()
existingRoles = [
r for r in allRoles
if r.roleLabel == roleLabel
and r.mandateId is None
and r.featureCode == featureCode
]
if existingRoles:
if updateExisting:
# Update existing role
existingRole = existingRoles[0]
roleId = existingRole.get("id")
roleId = existingRole.id
rootInterface.db.recordModify(
Role,
@ -315,41 +311,38 @@ async def export_mandate_rbac(
try:
rootInterface = getRootInterface()
# Get mandate-level roles
allRoles = rootInterface.db.getRecordset(Role)
# Get mandate-level roles using interface method
allRoles = rootInterface.getAllRoles()
mandateRoles = [
r for r in allRoles
if str(r.get("mandateId")) == str(context.mandateId)
if str(r.mandateId) == str(context.mandateId)
]
# Filter by feature instance if not including them
if not includeFeatureInstances:
mandateRoles = [r for r in mandateRoles if not r.get("featureInstanceId")]
mandateRoles = [r for r in mandateRoles if not r.featureInstanceId]
exportRoles = []
for role in mandateRoles:
roleId = role.get("id")
roleId = role.id
# Get access rules for this role
accessRules = rootInterface.db.getRecordset(
AccessRule,
recordFilter={"roleId": roleId}
)
# Get access rules for this role using interface method
accessRules = rootInterface.getAccessRulesByRole(roleId)
exportRoles.append(RoleExport(
roleLabel=role.get("roleLabel"),
description=role.get("description", {}),
featureCode=role.get("featureCode"),
isSystemRole=role.get("isSystemRole", False),
roleLabel=role.roleLabel,
description=role.description or {},
featureCode=role.featureCode,
isSystemRole=role.isSystemRole,
accessRules=[
{
"context": r.get("context"),
"item": r.get("item"),
"view": r.get("view", False),
"read": r.get("read"),
"create": r.get("create"),
"update": r.get("update"),
"delete": r.get("delete")
"context": r.context,
"item": r.item,
"view": r.view if r.view is not None else False,
"read": r.read,
"create": r.create,
"update": r.update,
"delete": r.delete
}
for r in accessRules
]
@ -453,21 +446,20 @@ async def import_mandate_rbac(
result.rolesSkipped += 1
continue
# Check if role exists (mandate role with same label)
existingRoles = rootInterface.db.getRecordset(
Role,
recordFilter={
"roleLabel": roleLabel,
"mandateId": str(context.mandateId),
"featureInstanceId": None # Only mandate-level roles
}
)
# Check if role exists (mandate role with same label) using interface method
allRoles = rootInterface.getAllRoles()
existingRoles = [
r for r in allRoles
if r.roleLabel == roleLabel
and str(r.mandateId) == str(context.mandateId)
and r.featureInstanceId is None # Only mandate-level roles
]
if existingRoles:
if updateExisting:
# Update existing role
existingRole = existingRoles[0]
roleId = existingRole.get("id")
roleId = existingRole.id
rootInterface.db.recordModify(
Role,
@ -556,12 +548,11 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
rootInterface = getRootInterface()
for roleId in context.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
roleLabel = role.get("roleLabel", "")
role = rootInterface.getRole(roleId)
if role:
roleLabel = role.roleLabel
# Admin role at mandate level
if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
if roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
return True
return False
@ -580,10 +571,10 @@ def _updateAccessRules(interface, roleId: str, newRules: List[Dict[str, Any]]) -
Number of rules created/updated
"""
try:
# Delete existing rules for this role
existingRules = interface.db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
# Delete existing rules for this role using interface method
existingRules = interface.getAccessRulesByRole(roleId)
for rule in existingRules:
interface.db.recordDelete(AccessRule, rule.get("id"))
interface.db.recordDelete(AccessRule, rule.id)
# Create new rules
count = 0

View file

@ -36,25 +36,17 @@ def _getUserRoleLabels(interface, userId: str) -> List[str]:
"""
roleLabels: Set[str] = set()
# Get all UserMandate records for this user
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
# Get all UserMandate records for this user (Pydantic models)
userMandates = interface.getUserMandates(userId)
for um in userMandates:
userMandateId = um.get("id")
if not userMandateId:
continue
# Get all UserMandateRole records for this membership
userMandateRoles = interface.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": str(userMandateId)}
)
# Get all UserMandateRole records for this membership (Pydantic models)
userMandateRoles = interface.getUserMandateRoles(str(um.id))
for umr in userMandateRoles:
roleId = umr.get("roleId")
if roleId:
if umr.roleId:
# Get role by ID to get roleLabel
role = interface.getRole(str(roleId))
role = interface.getRole(str(umr.roleId))
if role:
roleLabels.add(role.roleLabel)
@ -362,21 +354,13 @@ async def list_users_with_roles(
try:
interface = getRootInterface()
# Get all users (SysAdmin sees all)
# Use db.getRecordset with UserInDB (the actual database model)
allUsersData = interface.db.getRecordset(UserInDB)
# Convert to User objects, filtering out sensitive fields
users = []
for u in allUsersData:
cleanedUser = {k: v for k, v in u.items() if not k.startswith("_") and k != "hashedPassword" and k != "resetToken" and k != "resetTokenExpires"}
if cleanedUser.get("roleLabels") is None:
cleanedUser["roleLabels"] = []
users.append(User(**cleanedUser))
# Get all users via interface method (Pydantic models)
users = interface.getAllUsers()
# Filter by mandate if specified (via UserMandate table)
if mandateId:
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
mandateUserIds = {str(um["userId"]) for um in userMandates}
userMandates = interface.getUserMandatesByMandate(mandateId)
mandateUserIds = {str(um.userId) for um in userMandates}
users = [u for u in users if str(u.id) in mandateUserIds]
# Filter by role if specified (via UserMandateRole)
@ -499,21 +483,18 @@ async def update_user_roles(
logger.warning(f"Non-standard role label assigned: {roleLabel}")
# Get user's first mandate (for role assignment)
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
userMandates = interface.getUserMandates(userId)
if not userMandates:
raise HTTPException(
status_code=400,
detail=f"User {userId} has no mandate memberships. Add to mandate first."
)
userMandateId = str(userMandates[0].get("id"))
userMandateId = str(userMandates[0].id)
# Get current roles for this mandate
existingRoles = interface.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": userMandateId}
)
existingRoleIds = {str(r.get("roleId")) for r in existingRoles}
# Get current roles for this mandate (Pydantic models)
existingRoles = interface.getUserMandateRoles(userMandateId)
existingRoleIds = {str(r.roleId) for r in existingRoles}
# Convert roleLabels to roleIds
newRoleIds = set()
@ -524,8 +505,8 @@ async def update_user_roles(
# Remove roles that are no longer needed
for existingRole in existingRoles:
if str(existingRole.get("roleId")) not in newRoleIds:
interface.db.recordDelete(UserMandateRole, str(existingRole.get("id")))
if str(existingRole.roleId) not in newRoleIds:
interface.removeRoleFromUserMandate(userMandateId, str(existingRole.roleId))
# Add new roles
for roleId in newRoleIds:
@ -596,25 +577,22 @@ async def add_user_role(
)
# Get user's first mandate
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
userMandates = interface.getUserMandates(userId)
if not userMandates:
raise HTTPException(
status_code=400,
detail=f"User {userId} has no mandate memberships. Add to mandate first."
)
userMandateId = str(userMandates[0].get("id"))
userMandateId = str(userMandates[0].id)
# Check if role is already assigned
existingAssignment = interface.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": userMandateId, "roleId": str(role.id)}
)
# Check if role is already assigned - use interface method
existingRoles = interface.getUserMandateRoles(userMandateId)
roleAlreadyAssigned = any(str(r.roleId) == str(role.id) for r in existingRoles)
if not existingAssignment:
# Add the role
newRole = UserMandateRole(userMandateId=userMandateId, roleId=str(role.id))
interface.db.recordCreate(UserMandateRole, newRole.model_dump())
if not roleAlreadyAssigned:
# Add the role via interface method
interface.addRoleToUserMandate(userMandateId, str(role.id))
logger.info(f"Added role {roleLabel} to user {userId} by SysAdmin {currentUser.id}")
userRoleLabels = _getUserRoleLabels(interface, userId)
@ -678,20 +656,14 @@ async def remove_user_role(
)
# Remove role from all user's mandates
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
userMandates = interface.getUserMandates(userId)
roleRemoved = False
for um in userMandates:
userMandateId = str(um.get("id"))
userMandateId = str(um.id)
# Find and delete the role assignment
assignments = interface.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": userMandateId, "roleId": str(role.id)}
)
for assignment in assignments:
interface.db.recordDelete(UserMandateRole, str(assignment.get("id")))
# Remove role via interface method
if interface.removeRoleFromUserMandate(userMandateId, str(role.id)):
roleRemoved = True
if roleRemoved:
@ -751,25 +723,21 @@ async def get_users_with_role(
detail=f"Role '{roleLabel}' not found"
)
# Get all UserMandateRole assignments for this role
roleAssignments = interface.db.getRecordset(
UserMandateRole,
recordFilter={"roleId": str(role.id)}
)
# Get all UserMandateRole assignments for this role (Pydantic models)
roleAssignments = interface.getUserMandateRolesByRole(str(role.id))
# Get unique userMandateIds
userMandateIds = {str(ra.get("userMandateId")) for ra in roleAssignments}
userMandateIds = {str(ra.userMandateId) for ra in roleAssignments}
# Get userIds from UserMandate records
userIds: Set[str] = set()
for userMandateId in userMandateIds:
umRecords = interface.db.getRecordset(UserMandate, recordFilter={"id": userMandateId})
if umRecords:
um = umRecords[0]
um = interface.getUserMandateById(userMandateId)
if um:
# Filter by mandate if specified
if mandateId and str(um.get("mandateId")) != mandateId:
if mandateId and str(um.mandateId) != mandateId:
continue
userIds.add(str(um.get("userId")))
userIds.add(str(um.userId))
# Get users and format response
result = []

View file

@ -179,17 +179,15 @@ async def get_all_permissions(
# For UI/RESOURCE: Load system roles the user has across ALL their mandates
# This allows users to access system UI elements without needing a specific mandate header
userMandates = rootInterface.db.getRecordset(
UserMandate,
recordFilter={"userId": str(reqContext.user.id), "enabled": True}
)
allUserMandates = rootInterface.getUserMandates(str(reqContext.user.id))
userMandates = [um for um in allUserMandates if um.enabled]
logger.debug(f"UI/RESOURCE permissions: Found {len(userMandates)} UserMandates for user {reqContext.user.id}")
# Collect all role IDs the user has across all mandates
for userMandate in userMandates:
mandateRoleIds = rootInterface.getRoleIdsForUserMandate(userMandate.get("id"))
logger.debug(f"UI/RESOURCE permissions: UserMandate {userMandate.get('id')} (mandate {userMandate.get('mandateId')}) has {len(mandateRoleIds)} roles: {mandateRoleIds}")
mandateRoleIds = rootInterface.getRoleIdsForUserMandate(userMandate.id)
logger.debug(f"UI/RESOURCE permissions: UserMandate {userMandate.id} (mandate {userMandate.mandateId}) has {len(mandateRoleIds)} roles: {mandateRoleIds}")
for rid in mandateRoleIds:
if rid not in roleIds:
roleIds.append(rid)
@ -210,14 +208,11 @@ async def get_all_permissions(
allRules[ctx] = []
# Get all rules for user's roles - bypass RBAC filtering
for roleId in roleIds:
ruleRecords = rootInterface.db.getRecordset(
AccessRule,
recordFilter={"roleId": str(roleId), "context": ctx.value}
)
for ruleRecord in ruleRecords:
# Convert dict to AccessRule object
cleanedRule = {k: v for k, v in ruleRecord.items() if not k.startswith("_")}
allRules[ctx].append(AccessRule(**cleanedRule))
# Use interface method and filter by context
rules = rootInterface.getAccessRulesByRole(str(roleId))
for rule in rules:
if rule.context == ctx.value:
allRules[ctx].append(rule)
# Build result: for each context, collect all unique items and calculate permissions
for ctx in contextsToFetch:
@ -405,14 +400,8 @@ async def get_access_rules_by_role(
try:
interface = getRootInterface()
# Build filter for roleId
recordFilter = {"roleId": roleId}
# Get rules from database
rules = interface.db.getRecordset(AccessRule, recordFilter=recordFilter)
# Convert to AccessRule objects
ruleObjects = [AccessRule(**rule) for rule in rules]
# Get rules from database using interface method
ruleObjects = interface.getAccessRulesByRole(roleId)
return PaginatedResponse(
items=[rule.model_dump() for rule in ruleObjects],
@ -1128,13 +1117,9 @@ async def getCatalogObjects(
if mandateId:
try:
interface = getRootInterface()
# Get all feature instances for this mandate
from modules.datamodels.datamodelFeatures import FeatureInstance
instances = interface.db.getRecordset(
FeatureInstance,
recordFilter={"mandateId": mandateId, "enabled": True}
)
activeFeatures = set(inst.get("featureCode") for inst in instances)
# Get all feature instances for this mandate using interface method
instances = interface.getFeatureInstancesByMandate(mandateId, enabledOnly=True)
activeFeatures = set(inst.featureCode for inst in instances)
# Always include "system" feature
activeFeatures.add("system")
except Exception as e:

View file

@ -47,11 +47,15 @@ def _getAccessLevelLabel(level: Optional[str]) -> str:
return labels.get(level, "-")
def _getRoleScope(role: Dict[str, Any]) -> str:
"""Determine the scope of a role."""
if role.get("featureInstanceId"):
def _getRoleScope(role) -> str:
"""Determine the scope of a role. Accepts Role object or dict."""
# Support both Pydantic models and dicts
featureInstanceId = getattr(role, 'featureInstanceId', None) or (role.get("featureInstanceId") if isinstance(role, dict) else None)
mandateId = getattr(role, 'mandateId', None) or (role.get("mandateId") if isinstance(role, dict) else None)
if featureInstanceId:
return "instance"
elif role.get("mandateId"):
elif mandateId:
return "mandate"
else:
return "global"
@ -79,18 +83,18 @@ async def listUsersForOverview(
try:
interface = getRootInterface()
# Get all users
allUsersData = interface.db.getRecordset(UserInDB)
# Get all users using interface method
allUsers = interface.getAllUsers()
result = []
for u in allUsersData:
for u in allUsers:
result.append({
"id": u.get("id"),
"username": u.get("username"),
"email": u.get("email"),
"fullName": u.get("fullName"),
"isSysAdmin": u.get("isSysAdmin", False),
"enabled": u.get("enabled", True),
"id": u.id,
"username": u.username,
"email": u.email,
"fullName": u.fullName,
"isSysAdmin": u.isSysAdmin,
"enabled": u.enabled,
})
# Sort by username
@ -172,47 +176,43 @@ async def getUserAccessOverview(
allRoles = []
roleIdToInfo = {} # Map roleId to role info for later reference
# Get mandates for this user
mandateFilter = {"userId": userId, "enabled": True}
# Get mandates for this user using interface method
allUserMandates = interface.getUserMandates(userId)
# Filter by enabled and optionally mandateId
userMandates = [um for um in allUserMandates if um.enabled]
if mandateId:
mandateFilter["mandateId"] = mandateId
userMandates = interface.db.getRecordset(UserMandate, recordFilter=mandateFilter)
userMandates = [um for um in userMandates if um.mandateId == mandateId]
mandatesInfo = []
for um in userMandates:
umId = um.get("id")
umMandateId = um.get("mandateId")
umId = um.id
umMandateId = um.mandateId
# Get mandate name
mandate = interface.getMandate(umMandateId)
mandateName = mandate.name if mandate else umMandateId
# Get roles for this UserMandate
umRoles = interface.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": umId}
)
# Get roles for this UserMandate using interface method
umRoles = interface.getUserMandateRoles(umId)
mandateRoleIds = []
for umr in umRoles:
roleId = umr.get("roleId")
roleId = umr.roleId
if roleId:
mandateRoleIds.append(roleId)
# Get role details
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
# Get role details using interface method
role = interface.getRole(roleId)
if role:
scope = _getRoleScope(role)
roleInfo = {
"id": roleId,
"roleLabel": role.get("roleLabel"),
"description": role.get("description", {}),
"roleLabel": role.roleLabel,
"description": role.description or {},
"scope": scope,
"scopePriority": _getRoleScopePriority(scope),
"mandateId": role.get("mandateId"),
"featureInstanceId": role.get("featureInstanceId"),
"mandateId": role.mandateId,
"featureInstanceId": role.featureInstanceId,
"source": "mandate",
"sourceMandateId": umMandateId,
"sourceMandateName": mandateName,
@ -220,69 +220,59 @@ async def getUserAccessOverview(
allRoles.append(roleInfo)
roleIdToInfo[roleId] = roleInfo
# Get feature instances for this mandate
featureInstanceFilter = {"userId": userId, "enabled": True}
featureAccesses = interface.db.getRecordset(FeatureAccess, recordFilter=featureInstanceFilter)
# Get feature instances for this mandate using interface method
allFeatureAccesses = interface.getFeatureAccessesForUser(userId)
featureAccesses = [fa for fa in allFeatureAccesses if fa.enabled]
featureInstancesInfo = []
for fa in featureAccesses:
faId = fa.get("id")
faInstanceId = fa.get("featureInstanceId")
faId = fa.id
faInstanceId = fa.featureInstanceId
# Check if instance belongs to this mandate
instance = interface.db.getRecordset(FeatureInstance, recordFilter={"id": faInstanceId})
# Check if instance belongs to this mandate using interface method
instance = interface.getFeatureInstance(faInstanceId)
if not instance:
continue
instance = instance[0]
if instance.get("mandateId") != umMandateId:
if instance.mandateId != umMandateId:
continue
# Filter by featureInstanceId if specified
if featureInstanceId and faInstanceId != featureInstanceId:
continue
# Get feature info
featureCode = instance.get("featureCode")
featureRecords = interface.db.getRecordset(Feature, recordFilter={"code": featureCode})
featureLabel = featureRecords[0].get("label", {}) if featureRecords else {}
# Get feature info using interface method
featureCode = instance.featureCode
feature = interface.getFeatureByCode(featureCode)
featureLabel = feature.label if feature else {}
# Get roles for this FeatureAccess
faRoles = interface.db.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": faId}
)
instanceRoleIds = []
for far in faRoles:
roleId = far.get("roleId")
if roleId:
instanceRoleIds.append(roleId)
# Get roles for this FeatureAccess using interface method
instanceRoleIds = interface.getRoleIdsForFeatureAccess(faId)
for roleId in instanceRoleIds:
# Get role details (if not already added)
if roleId not in roleIdToInfo:
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
role = interface.getRole(roleId)
if role:
scope = _getRoleScope(role)
roleInfo = {
"id": roleId,
"roleLabel": role.get("roleLabel"),
"description": role.get("description", {}),
"roleLabel": role.roleLabel,
"description": role.description or {},
"scope": scope,
"scopePriority": _getRoleScopePriority(scope),
"mandateId": role.get("mandateId"),
"featureInstanceId": role.get("featureInstanceId"),
"mandateId": role.mandateId,
"featureInstanceId": role.featureInstanceId,
"source": "featureInstance",
"sourceInstanceId": faInstanceId,
"sourceInstanceLabel": instance.get("label"),
"sourceInstanceLabel": instance.label,
}
allRoles.append(roleInfo)
roleIdToInfo[roleId] = roleInfo
featureInstancesInfo.append({
"id": faInstanceId,
"label": instance.get("label"),
"label": instance.label,
"featureCode": featureCode,
"featureLabel": featureLabel,
"roleIds": instanceRoleIds,
@ -317,12 +307,12 @@ async def getUserAccessOverview(
roleLabel = roleInfo.get("roleLabel", "unknown")
roleScope = roleInfo.get("scope", "unknown")
# Get all rules for this role
rules = interface.db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
# Get all rules for this role using interface method
rules = interface.getAccessRulesByRole(roleId)
for rule in rules:
context = rule.get("context")
item = rule.get("item")
context = rule.context
item = rule.item
accessEntry = {
"item": item or "(all)",
@ -333,20 +323,20 @@ async def getUserAccessOverview(
}
if context == "UI":
accessEntry["view"] = rule.get("view", False)
accessEntry["view"] = rule.view if rule.view is not None else False
if accessEntry["view"]:
uiAccess.append(accessEntry)
elif context == "DATA":
accessEntry["view"] = rule.get("view", False)
accessEntry["read"] = _getAccessLevelLabel(rule.get("read"))
accessEntry["create"] = _getAccessLevelLabel(rule.get("create"))
accessEntry["update"] = _getAccessLevelLabel(rule.get("update"))
accessEntry["delete"] = _getAccessLevelLabel(rule.get("delete"))
accessEntry["view"] = rule.view if rule.view is not None else False
accessEntry["read"] = _getAccessLevelLabel(rule.read)
accessEntry["create"] = _getAccessLevelLabel(rule.create)
accessEntry["update"] = _getAccessLevelLabel(rule.update)
accessEntry["delete"] = _getAccessLevelLabel(rule.delete)
dataAccess.append(accessEntry)
elif context == "RESOURCE":
accessEntry["view"] = rule.get("view", False)
accessEntry["view"] = rule.view if rule.view is not None else False
if accessEntry["view"]:
resourceAccess.append(accessEntry)

View file

@ -1,128 +0,0 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Chat Playground routes for the backend API.
Implements the endpoints for chat playground workflow management.
"""
import logging
from typing import Optional, Dict, Any
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Request
# Import auth modules
from modules.auth import limiter, getRequestContext, RequestContext
# Import interfaces
from modules.interfaces import interfaceDbChat
# Import models
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
# Import workflow control functions
from modules.workflows.automation import chatStart, chatStop
# Configure logger
logger = logging.getLogger(__name__)
# Create router for chat playground endpoints
router = APIRouter(
prefix="/api/chat/playground",
tags=["Chat Playground"],
responses={404: {"description": "Not found"}}
)
def _getServiceChat(context: RequestContext):
return interfaceDbChat.getInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None)
# Workflow start endpoint
@router.post("/start", response_model=ChatWorkflow)
@limiter.limit("120/minute")
async def start_workflow(
request: Request,
workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
workflowMode: WorkflowModeEnum = Query(..., description="Workflow mode: 'Dynamic' or 'Automation' (mandatory)"),
userInput: UserInputRequest = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> ChatWorkflow:
"""
Starts a new workflow or continues an existing one.
Corresponds to State 1 in the state machine documentation.
Args:
workflowMode: "Dynamic" for iterative dynamic-style processing, "Automation" for automated workflow execution
"""
try:
# Start or continue workflow using playground controller
mandateId = str(context.mandateId) if context.mandateId else None
workflow = await chatStart(context.user, userInput, workflowMode, workflowId, mandateId=mandateId)
return workflow
except Exception as e:
logger.error(f"Error in start_workflow: {str(e)}")
raise HTTPException(
status_code=500,
detail=str(e)
)
# State 8: Workflow Stopped endpoint
@router.post("/{workflowId}/stop", response_model=ChatWorkflow)
@limiter.limit("120/minute")
async def stop_workflow(
request: Request,
workflowId: str = Path(..., description="ID of the workflow to stop"),
context: RequestContext = Depends(getRequestContext)
) -> ChatWorkflow:
"""Stops a running workflow."""
try:
# Stop workflow using playground controller
mandateId = str(context.mandateId) if context.mandateId else None
workflow = await chatStop(context.user, workflowId, mandateId=mandateId)
return workflow
except Exception as e:
logger.error(f"Error in stop_workflow: {str(e)}")
raise HTTPException(
status_code=500,
detail=str(e)
)
# Unified Chat Data Endpoint for Polling
@router.get("/{workflowId}/chatData")
@limiter.limit("120/minute")
async def get_workflow_chat_data(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
afterTimestamp: Optional[float] = Query(None, description="Unix timestamp to get data after"),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""
Get unified chat data (messages, logs, stats) for a workflow with timestamp-based selective data transfer.
Returns all data types in chronological order based on _createdAt timestamp.
"""
try:
# Get service center
interfaceDbChat = _getServiceChat(context)
# Verify workflow exists
workflow = interfaceDbChat.getWorkflow(workflowId)
if not workflow:
raise HTTPException(
status_code=404,
detail=f"Workflow with ID {workflowId} not found"
)
# Get unified chat data using the new method
chatData = interfaceDbChat.getUnifiedChatData(workflowId, afterTimestamp)
return chatData
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting unified chat data: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Error getting unified chat data: {str(e)}"
)

View file

@ -43,30 +43,14 @@ def getTokenStatusForConnection(interface, connectionId: str) -> tuple[str, Opti
- tokenExpiresAt: UTC timestamp or None
"""
try:
# Query tokens table for the latest token for this connection
tokens = interface.db.getRecordset(
Token,
recordFilter={"connectionId": connectionId}
)
if not tokens:
return "none", None
# Find the most recent token (highest createdAt timestamp)
latestToken = None
latestCreatedAt = 0
for tokenData in tokens:
createdAt = parseTimestamp(tokenData.get("createdAt"), default=0)
if createdAt > latestCreatedAt:
latestCreatedAt = createdAt
latestToken = tokenData
# Query tokens table for the latest token for this connection using interface method
latestToken = interface.getConnectionToken(connectionId)
if not latestToken:
return "none", None
# Check if token is expired
expiresAt = parseTimestamp(latestToken.get("expiresAt"))
expiresAt = parseTimestamp(latestToken.expiresAt)
if not expiresAt:
return "none", None

View file

@ -291,9 +291,9 @@ async def delete_mandate(
)
# MULTI-TENANT: Delete all UserMandate entries for this mandate first
userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
userMandates = appInterface.getUserMandatesByMandate(mandateId)
for um in userMandates:
appInterface.db.deleteRecord(UserMandate, um["id"])
appInterface.deleteUserMandate(str(um.userId), mandateId)
logger.info(f"Deleted {len(userMandates)} UserMandate entries for mandate {mandateId}")
# Delete mandate
@ -377,39 +377,46 @@ async def list_mandate_users(
)
# Get all UserMandate entries for this mandate
userMandates = rootInterface.db.getRecordset(
UserMandate,
recordFilter={"mandateId": targetMandateId}
)
userMandates = rootInterface.getUserMandatesByMandate(targetMandateId)
result = []
for um in userMandates:
# Get user info
user = rootInterface.getUser(um.get("userId"))
user = rootInterface.getUser(str(um.userId))
if not user:
continue
# Get roles for this membership
roleIds = rootInterface.getRoleIdsForUserMandate(um.get("id"))
roleIds = rootInterface.getRoleIdsForUserMandate(str(um.id))
# Resolve role labels for display
# Resolve role labels for display (only mandate-level roles, deduplicated)
roleLabels = []
filteredRoleIds = []
seenLabels = set()
for roleId in roleIds:
role = rootInterface.getRole(roleId)
if role:
# Skip feature-instance roles - they don't belong in mandate membership
if role.featureInstanceId:
continue
filteredRoleIds.append(roleId)
if role.roleLabel not in seenLabels:
roleLabels.append(role.roleLabel)
seenLabels.add(role.roleLabel)
else:
roleLabels.append(roleId) # Fallback to ID if not found
# Role not found - fail-safe: skip (no access)
logger.warning(f"Role {roleId} not found, skipping")
continue
result.append({
"id": um.get("id"), # UserMandate ID as primary key
"id": str(um.id), # UserMandate ID as primary key
"userId": str(user.id),
"username": user.username,
"email": user.email,
"fullName": user.fullName,
"roleIds": roleIds,
"roleIds": filteredRoleIds,
"roleLabels": roleLabels,
"enabled": um.get("enabled", True)
"enabled": um.enabled
})
# Apply search, filtering, and sorting if pagination requested
@ -545,18 +552,12 @@ async def add_user_to_mandate(
# 6. Validate roles (must exist and belong to this mandate or be global)
for roleId in data.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if not roleRecords:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Role {roleId} not found"
)
role = roleRecords[0]
roleMandateId = role.get("mandateId")
if roleMandateId and str(roleMandateId) != str(targetMandateId):
try:
rootInterface.validateRoleForMandate(roleId, targetMandateId)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Role {roleId} belongs to a different mandate"
detail=str(e)
)
# 7. Create UserMandate
@ -718,18 +719,12 @@ async def update_user_roles_in_mandate(
# Validate new roles
for roleId in roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if not roleRecords:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Role {roleId} not found"
)
role = roleRecords[0]
roleMandateId = role.get("mandateId")
if roleMandateId and str(roleMandateId) != str(targetMandateId):
try:
rootInterface.validateRoleForMandate(roleId, targetMandateId)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Role {roleId} belongs to a different mandate"
detail=str(e)
)
# Check if removing admin role would leave mandate without admins
@ -745,12 +740,7 @@ async def update_user_roles_in_mandate(
)
# Remove existing role assignments
existingRoles = rootInterface.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": str(membership.id)}
)
for er in existingRoles:
rootInterface.db.recordDelete(UserMandateRole, er.get("id"))
rootInterface.deleteUserMandateRoles(str(membership.id))
# Add new role assignments
for roleId in roleIds:
@ -812,19 +802,17 @@ def _hasMandateAdminRole(context: RequestContext, mandateId: str) -> bool:
rootInterface = interfaceDbApp.getRootInterface()
for roleId in context.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
roleLabel = role.get("roleLabel", "")
role = rootInterface.getRole(roleId)
if role:
# Admin role at mandate level (not feature-instance level)
if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
if role.roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
return True
return False
except Exception as e:
logger.error(f"Error checking mandate admin role: {e}")
return False
return False # Fail-safe: no access on error
def _isLastMandateAdmin(interface, mandateId: str, excludeUserId: str) -> bool:
@ -832,19 +820,17 @@ def _isLastMandateAdmin(interface, mandateId: str, excludeUserId: str) -> bool:
Check if excluding this user would leave the mandate without any admins.
"""
try:
# Get all UserMandates for this mandate
userMandates = interface.db.getRecordset(
UserMandate,
recordFilter={"mandateId": mandateId, "enabled": True}
)
# Get all UserMandates for this mandate (Pydantic models)
allMandates = interface.getUserMandatesByMandate(mandateId)
userMandates = [um for um in allMandates if um.enabled]
adminCount = 0
for um in userMandates:
if str(um.get("userId")) == str(excludeUserId):
if str(um.userId) == str(excludeUserId):
continue
# Check if this user has admin role
roleIds = interface.getRoleIdsForUserMandate(um.get("id"))
roleIds = interface.getRoleIdsForUserMandate(str(um.id))
if _hasAdminRoleInList(interface, roleIds, mandateId):
adminCount += 1
@ -852,7 +838,7 @@ def _isLastMandateAdmin(interface, mandateId: str, excludeUserId: str) -> bool:
except Exception as e:
logger.error(f"Error checking last admin: {e}")
return True # Fail-safe: assume they're the last admin
return True # Fail-safe: assume they're the last admin (prevents deletion)
def _hasAdminRoleInList(interface, roleIds: List[str], mandateId: str) -> bool:
@ -860,13 +846,10 @@ def _hasAdminRoleInList(interface, roleIds: List[str], mandateId: str) -> bool:
Check if any of the role IDs is an admin role for the mandate.
"""
for roleId in roleIds:
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
roleLabel = role.get("roleLabel", "")
roleMandateId = role.get("mandateId")
# Admin role at mandate level
if roleLabel == "admin" and (not roleMandateId or str(roleMandateId) == str(mandateId)):
if not role.get("featureInstanceId"):
role = interface.getRole(roleId)
if role:
# Admin role at mandate level (global or mandate-specific, not feature-instance)
if role.roleLabel == "admin" and not role.featureInstanceId:
if not role.mandateId or str(role.mandateId) == str(mandateId):
return True
return False

View file

@ -21,7 +21,8 @@ import modules.interfaces.interfaceDbApp as interfaceDbApp
from modules.auth import limiter, getRequestContext, RequestContext
# Import the attribute definition and helper functions
from modules.datamodels.datamodelUam import User, UserInDB
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
# Configure logger
@ -251,16 +252,10 @@ async def get_users(
)
elif context.isSysAdmin:
# SysAdmin without mandateId sees all users
# Get all users directly from database using UserInDB (the actual database model)
allUsers = appInterface.db.getRecordset(UserInDB)
# Convert to cleaned dictionaries first for filtering
cleanedUsers = []
for u in allUsers:
cleanedUser = {k: v for k, v in u.items() if not k.startswith("_") and k != "hashedPassword" and k != "resetToken" and k != "resetTokenExpires"}
# Ensure roleLabels is always a list
if cleanedUser.get("roleLabels") is None:
cleanedUser["roleLabels"] = []
cleanedUsers.append(cleanedUser)
# Get all users via interface method (returns Pydantic User models)
allUserModels = appInterface.getAllUsers()
# Convert to dictionaries for filtering/sorting
cleanedUsers = [u.model_dump() for u in allUserModels]
# Apply server-side filtering and sorting
filteredUsers = _applyFiltersAndSort(cleanedUsers, paginationParams)
@ -331,11 +326,7 @@ async def get_user(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@ -427,11 +418,7 @@ async def update_user(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@ -482,11 +469,7 @@ async def reset_user_password(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@ -664,11 +647,7 @@ async def send_password_link(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@ -791,11 +770,7 @@ async def delete_user(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@ -803,10 +778,9 @@ async def delete_user(
)
# Delete UserMandate entries for this user first
from modules.datamodels.datamodelMembership import UserMandate
userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
userMandates = appInterface.getUserMandates(userId)
for um in userMandates:
appInterface.db.deleteRecord(UserMandate, um["id"])
appInterface.deleteUserMandate(userId, str(um.mandateId))
success = appInterface.deleteUser(userId)
if not success:

View file

@ -163,16 +163,14 @@ async def update_workflow(
# Get workflow interface with current user context
workflowInterface = getInterface(currentUser)
# Get raw workflow data from database to check permissions
workflows = workflowInterface.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId})
if not workflows:
# Get workflow using interface method to check permissions
workflow = workflowInterface.getWorkflow(workflowId)
if not workflow:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Workflow not found"
)
workflow_data = workflows[0]
# Check if user has permission to update using RBAC
if not workflowInterface.checkRbacPermission(ChatWorkflow, "update", workflowId):
raise HTTPException(
@ -230,6 +228,49 @@ async def get_workflow_status(
detail=f"Error getting workflow status: {str(e)}"
)
# API Endpoint for stopping a workflow
@router.post("/{workflowId}/stop", response_model=ChatWorkflow)
@limiter.limit("120/minute")
async def stop_workflow(
request: Request,
workflowId: str = Path(..., description="ID of the workflow to stop"),
currentUser: User = Depends(getCurrentUser)
) -> ChatWorkflow:
"""
Stop a running workflow.
This is a general endpoint that can be used by any feature to stop a workflow.
"""
try:
from modules.workflows.automation import chatStop
# Get the workflow first to get mandateId
interfaceChatDb = getServiceChat(currentUser)
workflow = interfaceChatDb.getWorkflow(workflowId)
if not workflow:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workflow with ID {workflowId} not found"
)
mandateId = workflow.get("mandateId") if isinstance(workflow, dict) else getattr(workflow, "mandateId", None)
# Stop the workflow
stoppedWorkflow = await chatStop(currentUser, workflowId, mandateId=mandateId)
return stoppedWorkflow
except HTTPException:
raise
except Exception as e:
logger.error(f"Error stopping workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error stopping workflow: {str(e)}"
)
# API Endpoint for workflow logs with selective data transfer
@router.get("/{workflowId}/logs", response_model=PaginatedResponse[ChatLog])
@limiter.limit("120/minute")

View file

@ -109,96 +109,73 @@ async def export_user_data(
"authenticationAuthority": str(getattr(currentUser, "authenticationAuthority", ""))
}
# Mandate memberships
from modules.datamodels.datamodelMembership import UserMandate
userMandates = rootInterface.db.getRecordset(
UserMandate,
recordFilter={"userId": str(currentUser.id)}
)
# Mandate memberships using interface method
userMandates = rootInterface.getUserMandates(str(currentUser.id))
mandates = []
for um in userMandates:
mandateId = um.get("mandateId")
mandateId = um.mandateId
# Get mandate details
mandateRecords = rootInterface.db.getRecordset(
Mandate,
recordFilter={"id": mandateId}
)
mandateName = mandateRecords[0].get("name") if mandateRecords else "Unknown"
# Get mandate details using interface method
mandate = rootInterface.getMandate(mandateId)
mandateName = mandate.name if mandate else "Unknown"
# Get roles for this membership
roleIds = rootInterface.getRoleIdsForUserMandate(um.get("id"))
roleIds = rootInterface.getRoleIdsForUserMandate(um.id)
mandates.append({
"userMandateId": um.get("id"),
"userMandateId": um.id,
"mandateId": mandateId,
"mandateName": mandateName,
"enabled": um.get("enabled", True),
"enabled": um.enabled,
"roleIds": roleIds,
"joinedAt": um.get("createdAt")
"joinedAt": um.createdAt
})
# Feature access records
from modules.datamodels.datamodelMembership import FeatureAccess
featureAccesses = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"userId": str(currentUser.id)}
)
# Feature access records using interface method
featureAccesses = rootInterface.getFeatureAccessesForUser(str(currentUser.id))
featureAccessList = []
for fa in featureAccesses:
instanceId = fa.get("featureInstanceId")
instanceId = fa.featureInstanceId
# Get instance details
from modules.datamodels.datamodelFeatures import FeatureInstance
instanceRecords = rootInterface.db.getRecordset(
FeatureInstance,
recordFilter={"id": instanceId}
)
# Get instance details using interface method
instance = rootInterface.getFeatureInstance(instanceId)
instanceInfo = instanceRecords[0] if instanceRecords else {}
roleIds = rootInterface.getRoleIdsForFeatureAccess(fa.get("id"))
roleIds = rootInterface.getRoleIdsForFeatureAccess(fa.id)
featureAccessList.append({
"featureAccessId": fa.get("id"),
"featureAccessId": fa.id,
"featureInstanceId": instanceId,
"featureCode": instanceInfo.get("featureCode"),
"instanceLabel": instanceInfo.get("label"),
"enabled": fa.get("enabled", True),
"featureCode": instance.featureCode if instance else None,
"instanceLabel": instance.label if instance else None,
"enabled": fa.enabled,
"roleIds": roleIds
})
# Invitations created by user
from modules.datamodels.datamodelInvitation import Invitation
invitationsCreated = rootInterface.db.getRecordset(
Invitation,
recordFilter={"createdBy": str(currentUser.id)}
)
# Invitations created by user using interface method
invitationsCreated = rootInterface.getInvitationsByCreator(str(currentUser.id))
invitationsCreatedList = [
{
"id": inv.get("id"),
"mandateId": inv.get("mandateId"),
"createdAt": inv.get("createdAt"),
"expiresAt": inv.get("expiresAt"),
"maxUses": inv.get("maxUses"),
"currentUses": inv.get("currentUses")
"id": inv.id,
"mandateId": inv.mandateId,
"createdAt": inv.createdAt,
"expiresAt": inv.expiresAt,
"maxUses": inv.maxUses,
"currentUses": inv.currentUses
}
for inv in invitationsCreated
]
# Invitations used by user
invitationsUsed = rootInterface.db.getRecordset(
Invitation,
recordFilter={"usedBy": str(currentUser.id)}
)
# Invitations used by user using interface method
invitationsUsed = rootInterface.getInvitationsByUsedBy(str(currentUser.id))
invitationsUsedList = [
{
"id": inv.get("id"),
"mandateId": inv.get("mandateId"),
"usedAt": inv.get("usedAt")
"id": inv.id,
"mandateId": inv.mandateId,
"usedAt": inv.usedAt
}
for inv in invitationsUsed
]
@ -262,26 +239,18 @@ async def export_portable_data(
"additionalProperty": []
}
# Add mandate memberships as organization affiliations
from modules.datamodels.datamodelMembership import UserMandate
userMandates = rootInterface.db.getRecordset(
UserMandate,
recordFilter={"userId": str(currentUser.id)}
)
# Add mandate memberships as organization affiliations using interface method
userMandates = rootInterface.getUserMandates(str(currentUser.id))
affiliations = []
for um in userMandates:
mandateRecords = rootInterface.db.getRecordset(
Mandate,
recordFilter={"id": um.get("mandateId")}
)
if mandateRecords:
mandate = mandateRecords[0]
mandate = rootInterface.getMandate(um.mandateId)
if mandate:
affiliations.append({
"@type": "Organization",
"identifier": um.get("mandateId"),
"name": mandate.get("name"),
"membershipActive": um.get("enabled", True)
"identifier": um.mandateId,
"name": mandate.name,
"membershipActive": um.enabled
})
if affiliations:
@ -370,15 +339,12 @@ async def delete_account(
# Step 2: Revoke invitations BEFORE generic deletion (business logic)
rootInterface = getRootInterface()
from modules.datamodels.datamodelInvitation import Invitation
userInvitations = rootInterface.db.getRecordset(
Invitation,
recordFilter={"createdBy": str(currentUser.id)}
)
userInvitations = rootInterface.getInvitationsByCreator(str(currentUser.id))
for inv in userInvitations:
rootInterface.db.recordModify(
Invitation,
inv.get("id"),
inv.id,
{"revokedAt": getUtcTimestamp()}
)

View file

@ -131,17 +131,14 @@ async def create_invitation(
# Validate role IDs exist and belong to this mandate or are global
for roleId in data.roleIds:
from modules.datamodels.datamodelRbac import Role
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if not roleRecords:
role = rootInterface.getRole(roleId)
if not role:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Role '{roleId}' not found"
)
role = roleRecords[0]
# Role must be global or belong to this mandate
roleMandateId = role.get("mandateId")
if roleMandateId and str(roleMandateId) != str(context.mandateId):
if role.mandateId and str(role.mandateId) != str(context.mandateId):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Role '{roleId}' belongs to a different mandate"
@ -149,18 +146,13 @@ async def create_invitation(
# Validate feature instance if provided
if data.featureInstanceId:
from modules.datamodels.datamodelFeatures import FeatureInstance
instanceRecords = rootInterface.db.getRecordset(
FeatureInstance,
recordFilter={"id": data.featureInstanceId}
)
if not instanceRecords:
instance = rootInterface.getFeatureInstance(data.featureInstanceId)
if not instance:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Feature instance '{data.featureInstanceId}' not found"
)
instance = instanceRecords[0]
if str(instance.get("mandateId")) != str(context.mandateId):
if str(instance.mandateId) != str(context.mandateId):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Feature instance belongs to a different mandate"
@ -196,14 +188,9 @@ async def create_invitation(
if data.email:
try:
from modules.connectors.connectorMessagingEmail import ConnectorMessagingEmail
from modules.datamodels.datamodelUam import Mandate
# Get mandate name for the email
mandateRecords = rootInterface.db.getRecordset(
Mandate,
recordFilter={"id": str(context.mandateId)}
)
mandateName = mandateRecords[0].get("name", "PowerOn") if mandateRecords else "PowerOn"
mandate = rootInterface.getMandate(str(context.mandateId))
mandateName = mandate.name if mandate else "PowerOn"
emailConnector = ConnectorMessagingEmail()
emailSubject = f"Einladung zu {mandateName}"
@ -259,14 +246,10 @@ async def create_invitation(
existingUser = rootInterface.getUserByUsername(data.targetUsername)
if existingUser:
from modules.routes.routeNotifications import createInvitationNotification
from modules.datamodels.datamodelUam import Mandate
# Get mandate name for notification
mandateRecords = rootInterface.db.getRecordset(
Mandate,
recordFilter={"id": str(context.mandateId)}
)
mandateName = mandateRecords[0].get("mandateLabel", "PowerOn") if mandateRecords else "PowerOn"
mandate = rootInterface.getMandate(str(context.mandateId))
mandateName = mandate.mandateLabel if mandate and mandate.mandateLabel else "PowerOn"
inviterName = context.user.fullName or context.user.username
createInvitationNotification(
@ -348,38 +331,38 @@ async def list_invitations(
try:
rootInterface = getRootInterface()
# Get all invitations for this mandate
allInvitations = rootInterface.db.getRecordset(
Invitation,
recordFilter={"mandateId": str(context.mandateId)}
)
# Get all invitations for this mandate (Pydantic models)
allInvitations = rootInterface.getInvitationsByMandate(str(context.mandateId))
currentTime = getUtcTimestamp()
result = []
for inv in allInvitations:
# Skip revoked invitations
if inv.get("revokedAt"):
if inv.revokedAt:
continue
# Filter by usage
if not includeUsed and inv.get("currentUses", 0) >= inv.get("maxUses", 1):
currentUses = inv.currentUses or 0
maxUses = inv.maxUses or 1
if not includeUsed and currentUses >= maxUses:
continue
# Filter by expiration
if not includeExpired and inv.get("expiresAt", 0) < currentTime:
expiresAt = inv.expiresAt or 0
if not includeExpired and expiresAt < currentTime:
continue
# Build invite URL
from modules.shared.configuration import APP_CONFIG
frontendUrl = APP_CONFIG.get("APP_FRONTEND_URL", "http://localhost:8080")
inviteUrl = f"{frontendUrl}/invite/{inv.get('token')}"
inviteUrl = f"{frontendUrl}/invite/{inv.token}"
result.append({
**{k: v for k, v in inv.items() if not k.startswith("_")},
**inv.model_dump(),
"inviteUrl": inviteUrl,
"isExpired": inv.get("expiresAt", 0) < currentTime,
"isUsedUp": inv.get("currentUses", 0) >= inv.get("maxUses", 1)
"isExpired": expiresAt < currentTime,
"isUsedUp": currentUses >= maxUses
})
return result
@ -425,29 +408,24 @@ async def revoke_invitation(
try:
rootInterface = getRootInterface()
# Get invitation
invitationRecords = rootInterface.db.getRecordset(
Invitation,
recordFilter={"id": invitationId}
)
# Get invitation (Pydantic model)
invitation = rootInterface.getInvitation(invitationId)
if not invitationRecords:
if not invitation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Invitation '{invitationId}' not found"
)
invitation = invitationRecords[0]
# Verify mandate access
if str(invitation.get("mandateId")) != str(context.mandateId):
if str(invitation.mandateId) != str(context.mandateId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to this invitation"
)
# Already revoked?
if invitation.get("revokedAt"):
if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation is already revoked"
@ -496,13 +474,10 @@ async def validate_invitation(
try:
rootInterface = getRootInterface()
# Find invitation by token
invitationRecords = rootInterface.db.getRecordset(
Invitation,
recordFilter={"token": token}
)
# Find invitation by token (Pydantic model)
invitation = rootInterface.getInvitationByToken(token)
if not invitationRecords:
if not invitation:
return InvitationValidation(
valid=False,
reason="Invitation not found",
@ -511,10 +486,8 @@ async def validate_invitation(
roleIds=[]
)
invitation = invitationRecords[0]
# Check if revoked
if invitation.get("revokedAt"):
if invitation.revokedAt:
return InvitationValidation(
valid=False,
reason="Invitation has been revoked",
@ -525,7 +498,8 @@ async def validate_invitation(
# Check if expired
currentTime = getUtcTimestamp()
if invitation.get("expiresAt", 0) < currentTime:
expiresAt = invitation.expiresAt or 0
if expiresAt < currentTime:
return InvitationValidation(
valid=False,
reason="Invitation has expired",
@ -535,7 +509,9 @@ async def validate_invitation(
)
# Check if used up
if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
currentUses = invitation.currentUses or 0
maxUses = invitation.maxUses or 1
if currentUses >= maxUses:
return InvitationValidation(
valid=False,
reason="Invitation has reached maximum uses",
@ -545,34 +521,29 @@ async def validate_invitation(
)
# Get additional info for display
mandateId = invitation.get("mandateId")
mandateId = invitation.mandateId
mandateName = None
roleLabels = []
targetUsername = invitation.get("targetUsername")
targetUsername = invitation.targetUsername
# Get mandate name
from modules.datamodels.datamodelUam import Mandate
mandateRecords = rootInterface.db.getRecordset(
Mandate,
recordFilter={"id": mandateId}
)
if mandateRecords:
mandateName = mandateRecords[0].get("name")
mandate = rootInterface.getMandate(str(mandateId)) if mandateId else None
if mandate:
mandateName = mandate.name
# Get role names
roleIds = invitation.get("roleIds", [])
from modules.datamodels.datamodelRbac import Role
roleIds = invitation.roleIds or []
for roleId in roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
roleLabels.append(roleRecords[0].get("roleLabel", roleId))
role = rootInterface.getRole(roleId)
if role:
roleLabels.append(role.roleLabel)
return InvitationValidation(
valid=True,
reason=None,
mandateId=mandateId,
mandateId=str(mandateId) if mandateId else None,
mandateName=mandateName,
featureInstanceId=invitation.get("featureInstanceId"),
featureInstanceId=str(invitation.featureInstanceId) if invitation.featureInstanceId else None,
roleIds=roleIds,
roleLabels=roleLabels,
targetUsername=targetUsername
@ -608,42 +579,40 @@ async def accept_invitation(
try:
rootInterface = getRootInterface()
# Find invitation by token
invitationRecords = rootInterface.db.getRecordset(
Invitation,
recordFilter={"token": token}
)
# Find invitation by token (Pydantic model)
invitation = rootInterface.getInvitationByToken(token)
if not invitationRecords:
if not invitation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invitation not found"
)
invitation = invitationRecords[0]
# Validate invitation
if invitation.get("revokedAt"):
if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has been revoked"
)
currentTime = getUtcTimestamp()
if invitation.get("expiresAt", 0) < currentTime:
expiresAt = invitation.expiresAt or 0
if expiresAt < currentTime:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has expired"
)
if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
currentUses = invitation.currentUses or 0
maxUses = invitation.maxUses or 1
if currentUses >= maxUses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has reached maximum uses"
)
# Validate username matches - the invitation is bound to a specific user
targetUsername = invitation.get("targetUsername")
targetUsername = invitation.targetUsername
if targetUsername and currentUser.username != targetUsername:
logger.warning(
f"User {currentUser.username} tried to accept invitation meant for {targetUsername}"
@ -653,9 +622,9 @@ async def accept_invitation(
detail=f"Diese Einladung ist für Benutzer '{targetUsername}' bestimmt"
)
mandateId = invitation.get("mandateId")
roleIds = invitation.get("roleIds", [])
featureInstanceId = invitation.get("featureInstanceId")
mandateId = str(invitation.mandateId) if invitation.mandateId else None
roleIds = invitation.roleIds or []
featureInstanceId = str(invitation.featureInstanceId) if invitation.featureInstanceId else None
# Check if user is already a member
existingMembership = rootInterface.getUserMandate(str(currentUser.id), mandateId)
@ -744,22 +713,19 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
try:
rootInterface = getRootInterface()
from modules.datamodels.datamodelRbac import Role
for roleId in context.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
roleLabel = role.get("roleLabel", "")
# Admin role at mandate level
if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
role = rootInterface.getRole(roleId)
if role:
# Admin role at mandate level (not feature-instance level)
if role.roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
return True
return False
except Exception as e:
logger.error(f"Error checking mandate admin role: {e}")
return False
return False # Fail-safe: no access on error
def _isInstanceRole(interface, roleId: str, featureInstanceId: str) -> bool:
@ -767,11 +733,9 @@ def _isInstanceRole(interface, roleId: str, featureInstanceId: str) -> bool:
Check if a role belongs to a specific feature instance.
"""
try:
from modules.datamodels.datamodelRbac import Role
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
return str(role.get("featureInstanceId", "")) == str(featureInstanceId)
role = interface.getRole(roleId)
if role:
return str(role.featureInstanceId or "") == str(featureInstanceId)
return False
except Exception:
return False
return False # Fail-safe: assume not instance role on error

View file

@ -421,10 +421,9 @@ def _hasTriggerPermission(context: RequestContext) -> bool:
rootInterface = getRootInterface()
for roleId in context.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roleRecords:
role = roleRecords[0]
roleLabel = role.get("roleLabel", "")
role = rootInterface.getRole(roleId)
if role:
roleLabel = role.roleLabel
# Admin role at mandate level or system admin
if roleLabel in ("admin", "sysadmin"):
return True

View file

@ -137,23 +137,19 @@ async def getNotifications(
# Build filter
recordFilter = {"userId": str(currentUser.id)}
if status:
recordFilter["status"] = status
if type:
recordFilter["type"] = type
# Get notifications
notifications = rootInterface.db.getRecordset(
model_class=UserNotification,
recordFilter=recordFilter
# Get notifications (Pydantic models, sorted and limited)
notifications = rootInterface.getNotificationsByUser(
userId=str(currentUser.id),
status=status,
limit=limit
)
# Sort by creation date (newest first) and limit
notifications = sorted(notifications, key=lambda x: x.get("createdAt", 0), reverse=True)
if limit:
notifications = notifications[:limit]
# Apply type filter if needed (not common, so filter post-fetch)
if type:
notifications = [n for n in notifications if n.type == type]
return notifications
# Convert to dicts for response
return [n.model_dump() for n in notifications]
except Exception as e:
logger.error(f"Error getting notifications: {e}")
@ -176,12 +172,10 @@ async def getUnreadCount(
try:
rootInterface = getRootInterface()
notifications = rootInterface.db.getRecordset(
model_class=UserNotification,
recordFilter={
"userId": str(currentUser.id),
"status": NotificationStatus.UNREAD.value
}
# Get unread notifications (Pydantic models)
notifications = rootInterface.getNotificationsByUser(
userId=str(currentUser.id),
status=NotificationStatus.UNREAD.value
)
return UnreadCountResponse(count=len(notifications))
@ -207,22 +201,17 @@ async def markAsRead(
try:
rootInterface = getRootInterface()
# Get the notification
notifications = rootInterface.db.getRecordset(
model_class=UserNotification,
recordFilter={"id": notificationId}
)
# Get the notification (Pydantic model)
notification = rootInterface.getNotification(notificationId)
if not notifications:
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
notification = notifications[0]
# Verify ownership
if notification.get("userId") != currentUser.id:
if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this notification"
@ -262,13 +251,10 @@ async def markAllAsRead(
try:
rootInterface = getRootInterface()
# Get all unread notifications
notifications = rootInterface.db.getRecordset(
model_class=UserNotification,
recordFilter={
"userId": currentUser.id,
"status": NotificationStatus.UNREAD.value
}
# Get all unread notifications (Pydantic models)
notifications = rootInterface.getNotificationsByUser(
userId=str(currentUser.id),
status=NotificationStatus.UNREAD.value
)
currentTime = getUtcTimestamp()
@ -277,7 +263,7 @@ async def markAllAsRead(
for notification in notifications:
rootInterface.db.recordModify(
model_class=UserNotification,
recordId=notification.get("id"),
recordId=str(notification.id),
record={
"status": NotificationStatus.READ.value,
"readAt": currentTime
@ -309,37 +295,32 @@ async def executeAction(
try:
rootInterface = getRootInterface()
# Get the notification
notifications = rootInterface.db.getRecordset(
model_class=UserNotification,
recordFilter={"id": notificationId}
)
# Get the notification (Pydantic model)
notification = rootInterface.getNotification(notificationId)
if not notifications:
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
notification = notifications[0]
# Verify ownership
if notification.get("userId") != currentUser.id:
if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this notification"
)
# Check if already actioned
if notification.get("status") == NotificationStatus.ACTIONED.value:
if notification.status == NotificationStatus.ACTIONED.value:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Notification has already been actioned"
)
# Validate action exists
actions = notification.get("actions", [])
validActionIds = [a.get("actionId") if isinstance(a, dict) else a.actionId for a in (actions or [])]
actions = notification.actions or []
validActionIds = [a.get("actionId") if isinstance(a, dict) else a.actionId for a in actions]
if actionRequest.actionId not in validActionIds:
raise HTTPException(
@ -407,22 +388,17 @@ async def _handleInvitationAction(
detail="No invitation reference found"
)
# Get the invitation
invitations = rootInterface.db.getRecordset(
model_class=Invitation,
recordFilter={"id": invitationId}
)
# Get the invitation (Pydantic model)
invitation = rootInterface.getInvitation(invitationId)
if not invitations:
if not invitation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invitation not found"
)
invitation = invitations[0]
# Verify username matches
if invitation.get("targetUsername") != currentUser.username:
if invitation.targetUsername != currentUser.username:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="This invitation is for a different user"
@ -430,19 +406,22 @@ async def _handleInvitationAction(
# Check if invitation is still valid
currentTime = getUtcTimestamp()
if invitation.get("expiresAt", 0) < currentTime:
expiresAt = invitation.expiresAt or 0
if expiresAt < currentTime:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has expired"
)
if invitation.get("revokedAt"):
if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has been revoked"
)
if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
currentUses = invitation.currentUses or 0
maxUses = invitation.maxUses or 1
if currentUses >= maxUses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has reached maximum uses"
@ -450,59 +429,34 @@ async def _handleInvitationAction(
if actionId == "accept":
# Accept the invitation - assign roles and mandate access
mandateId = invitation.get("mandateId")
roleIds = invitation.get("roleIds", [])
mandateId = str(invitation.mandateId) if invitation.mandateId else None
roleIds = list(invitation.roleIds or [])
# Ensure user gets the system "user" role for access to public UI elements (e.g. playground)
userRoles = rootInterface.db.getRecordset(
model_class=Role,
recordFilter={"roleLabel": "user"}
)
if userRoles:
userRoleId = userRoles[0].get("id")
userRole = rootInterface.getRoleByLabel("user")
if userRole:
userRoleId = str(userRole.id)
if userRoleId and userRoleId not in roleIds:
roleIds = roleIds + [userRoleId]
logger.debug(f"Added system 'user' role {userRoleId} to invitation roles")
# Get mandate name for result message
mandates = rootInterface.db.getRecordset(
model_class=Mandate,
recordFilter={"id": mandateId}
)
mandateName = mandates[0].get("mandateLabel", mandateId) if mandates else mandateId
mandate = rootInterface.getMandate(mandateId) if mandateId else None
mandateName = mandate.mandateLabel if mandate and mandate.mandateLabel else mandateId
# Check if user already has this mandate
existingMemberships = rootInterface.db.getRecordset(
model_class=UserMandate,
recordFilter={
"userId": currentUser.id,
"mandateId": mandateId
}
)
existingMembership = rootInterface.getUserMandate(str(currentUser.id), mandateId) if mandateId else None
if existingMemberships:
# Update existing membership with new roles
existingMembership = existingMemberships[0]
existingRoles = existingMembership.get("roleIds", [])
mergedRoles = list(set(existingRoles + roleIds))
rootInterface.db.recordModify(
model_class=UserMandate,
recordId=existingMembership.get("id"),
record={"roleIds": mergedRoles}
)
logger.info(f"Updated UserMandate for user {currentUser.id} in mandate {mandateId}")
if existingMembership:
# Update existing membership with new roles via interface
# Note: roleIds on UserMandate is deprecated - roles should be assigned via UserMandateRole
logger.info(f"User {currentUser.id} already has membership in mandate {mandateId}, adding roles via UserMandateRole")
# Add roles via junction table
for roleId in roleIds:
rootInterface.addRoleToUserMandate(str(existingMembership.id), roleId)
else:
# Create new user-mandate relationship
userMandate = UserMandate(
userId=currentUser.id,
mandateId=mandateId,
roleIds=roleIds
)
rootInterface.db.recordCreate(
model_class=UserMandate,
record=userMandate.model_dump()
)
# Create new user-mandate relationship via interface
rootInterface.createUserMandate(str(currentUser.id), mandateId, roleIds)
logger.info(f"Created UserMandate for user {currentUser.id} in mandate {mandateId}")
# Mark invitation as used
@ -510,9 +464,9 @@ async def _handleInvitationAction(
model_class=Invitation,
recordId=invitationId,
record={
"usedBy": currentUser.id,
"usedBy": str(currentUser.id),
"usedAt": currentTime,
"currentUses": invitation.get("currentUses", 0) + 1
"currentUses": currentUses + 1
}
)
@ -545,22 +499,17 @@ async def deleteNotification(
try:
rootInterface = getRootInterface()
# Get the notification
notifications = rootInterface.db.getRecordset(
model_class=UserNotification,
recordFilter={"id": notificationId}
)
# Get the notification (Pydantic model)
notification = rootInterface.getNotification(notificationId)
if not notifications:
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
notification = notifications[0]
# Verify ownership
if notification.get("userId") != currentUser.id:
if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this notification"

View file

@ -125,8 +125,8 @@ async def list_tokens(
if statusFilter:
recordFilter["status"] = statusFilter
# MULTI-TENANT: SysAdmin sees ALL tokens (no mandate filter)
tokens = appInterface.db.getRecordset(Token, recordFilter=recordFilter)
# Use interface method to get tokens with flexible filtering
tokens = appInterface.getAllTokens(recordFilter=recordFilter)
return tokens
except HTTPException:
raise
@ -254,15 +254,13 @@ async def revoke_tokens_by_mandate(
# MULTI-TENANT: SysAdmin can revoke tokens for any mandate
appInterface = getRootInterface()
# Get all UserMandate entries for this mandate to find users
# Note: In new model, users are linked via UserMandate, not User.mandateId
from modules.datamodels.datamodelMembership import UserMandate
userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
# Get all UserMandate entries for this mandate to find users using interface method
userMandates = appInterface.getUserMandatesByMandate(mandateId)
total = 0
for um in userMandates:
total += appInterface.revokeTokensByUser(
userId=um["userId"],
userId=um.userId,
authority=AuthAuthority(authority) if authority else None,
mandateId=None, # Revoke all tokens for user
revokedBy=currentUser.id,

View file

@ -15,7 +15,7 @@ import httpx
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
from modules.auth import getCurrentUser, limiter
from modules.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.auth import createAccessToken, setAccessTokenCookie, createRefreshToken, setRefreshTokenCookie
from modules.auth.tokenManager import TokenManager
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
@ -171,10 +171,9 @@ async def login(
try:
if connectionId:
rootInterface = getRootInterface()
records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId})
if records:
record = records[0]
login_hint = record.get("externalEmail") or record.get("externalUsername")
connection = rootInterface.getUserConnectionById(connectionId)
if connection:
login_hint = connection.externalEmail or connection.externalUsername
if login_hint:
extra_params["login_hint"] = login_hint
if "@" in login_hint:
@ -260,23 +259,20 @@ async def auth_callback(code: str, state: str, request: Request, response: Respo
rootInterface = getRootInterface()
# Prefer connection flow reuse; fallback to user access token
if connection_id:
existing_tokens = rootInterface.db.getRecordset(Token, recordFilter={
"connectionId": connection_id,
"authority": AuthAuthority.GOOGLE
})
existing_tokens = rootInterface.getTokensByConnectionIdAndAuthority(
connection_id, AuthAuthority.GOOGLE
)
if existing_tokens:
# Use most recent by createdAt
existing_tokens.sort(key=lambda x: parseTimestamp(x.get("createdAt"), default=0), reverse=True)
token_response["refresh_token"] = existing_tokens[0].get("tokenRefresh", "")
existing_tokens.sort(key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True)
token_response["refresh_token"] = existing_tokens[0].tokenRefresh or ""
if not token_response.get("refresh_token") and user_id:
existing_access_tokens = rootInterface.db.getRecordset(Token, recordFilter={
"userId": user_id,
"connectionId": None,
"authority": AuthAuthority.GOOGLE
})
existing_access_tokens = rootInterface.getTokensByUserIdNoConnection(
user_id, AuthAuthority.GOOGLE
)
if existing_access_tokens:
existing_access_tokens.sort(key=lambda x: parseTimestamp(x.get("createdAt"), default=0), reverse=True)
token_response["refresh_token"] = existing_access_tokens[0].get("tokenRefresh", "")
existing_access_tokens.sort(key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True)
token_response["refresh_token"] = existing_access_tokens[0].tokenRefresh or ""
except Exception:
# Non-fatal; continue without refresh token
pass

View file

@ -330,40 +330,34 @@ Falls Sie sich nicht registriert haben, können Sie diese E-Mail ignorieren."""
from modules.datamodels.datamodelUam import Mandate
currentTime = getUtcTimestamp()
pendingInvitations = appInterface.db.getRecordset(
model_class=Invitation,
recordFilter={"targetUsername": userData.username}
)
pendingInvitations = appInterface.getInvitationsByTargetUsername(userData.username)
for invitation in pendingInvitations:
# Skip expired, revoked, or fully used invitations
if invitation.get("expiresAt", 0) < currentTime:
if (invitation.expiresAt or 0) < currentTime:
continue
if invitation.get("revokedAt"):
if invitation.revokedAt:
continue
if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
if (invitation.currentUses or 0) >= (invitation.maxUses or 1):
continue
# Get mandate name for notification
mandateId = invitation.get("mandateId")
mandateRecords = appInterface.db.getRecordset(
Mandate,
recordFilter={"id": mandateId}
)
mandateName = mandateRecords[0].get("mandateLabel", "PowerOn") if mandateRecords else "PowerOn"
# Get mandate name for notification using interface method
mandateId = invitation.mandateId
mandate = appInterface.getMandate(mandateId)
mandateName = mandate.mandateLabel if mandate else "PowerOn"
# Get inviter name
inviterId = invitation.get("createdBy")
inviterId = invitation.createdBy
inviter = appInterface.getUserById(inviterId) if inviterId else None
inviterName = (inviter.fullName or inviter.username) if inviter else "PowerOn"
createInvitationNotification(
userId=str(user.id),
invitationId=str(invitation.get("id")),
invitationId=str(invitation.id),
mandateName=mandateName,
inviterName=inviterName
)
logger.info(f"Created notification for new user {userData.username} for invitation {invitation.get('id')}")
logger.info(f"Created notification for new user {userData.username} for invitation {invitation.id}")
except Exception as notifErr:
logger.warning(f"Failed to create notifications for pending invitations: {notifErr}")

View file

@ -16,7 +16,7 @@ from modules.shared.configuration import APP_CONFIG
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
from modules.datamodels.datamodelSecurity import Token
from modules.auth import getCurrentUser, limiter
from modules.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.auth import createAccessToken, setAccessTokenCookie, createRefreshToken, setRefreshTokenCookie
from modules.auth.tokenManager import TokenManager
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
@ -97,11 +97,10 @@ async def login(
if connectionId:
try:
rootInterface = getRootInterface()
# Fetch the connection by ID directly
records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId})
if records:
record = records[0]
login_hint = record.get("externalEmail") or record.get("externalUsername")
# Fetch the connection by ID directly using interface method
connection = rootInterface.getUserConnectionById(connectionId)
if connection:
login_hint = connection.externalEmail or connection.externalUsername
if login_hint:
login_kwargs["login_hint"] = login_hint
# Derive domain hint from email/UPN

View file

@ -38,13 +38,13 @@ def _getUserRoleIds(userId: str) -> List[str]:
rootInterface = getRootInterface()
roleIds = []
userMandates = rootInterface.db.getRecordset(
UserMandate,
recordFilter={"userId": userId, "enabled": True}
)
# Get UserMandates as Pydantic models
userMandates = rootInterface.getUserMandates(userId)
for um in userMandates:
mandateRoleIds = rootInterface.getRoleIdsForUserMandate(um.get("id"))
if not um.enabled:
continue
mandateRoleIds = rootInterface.getRoleIdsForUserMandate(str(um.id))
for rid in mandateRoleIds:
if rid not in roleIds:
roleIds.append(rid)
@ -60,30 +60,24 @@ def _checkUiPermission(roleIds: List[str], objectKey: str) -> bool:
rootInterface = getRootInterface()
for roleId in roleIds:
# Get UI rules for this role
rules = rootInterface.db.getRecordset(
AccessRule,
recordFilter={"roleId": roleId, "context": "UI"}
)
# Get UI rules for this role (returns Pydantic AccessRule models)
rules = rootInterface.getAccessRules(roleId=roleId, context=AccessRuleContext.UI)
for rule in rules:
ruleItem = rule.get("item")
ruleView = rule.get("view", False)
if not ruleView:
if not rule.view:
continue
# Global rule (item=None) grants access to all UI
if ruleItem is None:
if rule.item is None:
return True
# Exact match
if ruleItem == objectKey:
if rule.item == objectKey:
return True
# Wildcard match (e.g., ui.system.* matches ui.system.playground)
if ruleItem.endswith(".*"):
prefix = ruleItem[:-2]
if rule.item.endswith(".*"):
prefix = rule.item[:-2]
if objectKey.startswith(prefix):
return True
@ -108,6 +102,12 @@ def _getFeatureUiObjects(featureCode: str) -> List[Dict[str, Any]]:
elif featureCode == "realestate":
from modules.features.realestate.mainRealEstate import UI_OBJECTS
return UI_OBJECTS
elif featureCode == "chatplayground":
from modules.features.chatplayground.mainChatplayground import UI_OBJECTS
return UI_OBJECTS
elif featureCode == "automation":
from modules.features.automation.mainAutomation import UI_OBJECTS
return UI_OBJECTS
else:
logger.warning(f"Unknown feature code: {featureCode}")
return []
@ -287,67 +287,50 @@ def _getInstanceViewPermissions(
permissions = {"_all": False, "isAdmin": False}
try:
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext, Role
# Get FeatureAccess for this user and instance (Pydantic model)
featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
# Get FeatureAccess for this user and instance
featureAccesses = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
if not featureAccesses:
if not featureAccess:
return permissions
# Get role IDs via FeatureAccessRole junction table
featureAccessId = featureAccesses[0].get("id")
featureAccessRoles = rootInterface.db.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
roleIds = [far.get("roleId") for far in featureAccessRoles]
# Get role IDs via interface method
roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
if not roleIds:
return permissions
# Check if user has admin role
for roleId in roleIds:
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
if roles:
roleLabel = roles[0].get("roleLabel", "").lower()
if "admin" in roleLabel:
role = rootInterface.getRole(roleId)
if role and "admin" in role.roleLabel.lower():
permissions["isAdmin"] = True
break
# Get UI permissions from AccessRules
# Permissions are stored with full objectKey (e.g., ui.feature.trustee.dashboard)
# Get UI permissions from AccessRules (Pydantic models)
for roleId in roleIds:
accessRules = rootInterface.db.getRecordset(
AccessRule,
recordFilter={"roleId": roleId, "context": "UI"}
)
accessRules = rootInterface.getAccessRules(roleId=roleId, context=AccessRuleContext.UI)
logger.debug(f"_getInstanceViewPermissions: roleId={roleId}, UI rules count={len(accessRules)}")
for rule in accessRules:
if not rule.get("view", False):
if not rule.view:
continue
item = rule.get("item")
logger.debug(f"_getInstanceViewPermissions: rule item={item}, view={rule.get('view')}")
logger.debug(f"_getInstanceViewPermissions: rule item={rule.item}, view={rule.view}")
if item is None:
if rule.item is None:
# item=None means all views
permissions["_all"] = True
else:
# Store full objectKey as per Navigation-API-Konzept
permissions[item] = True
permissions[rule.item] = True
logger.debug(f"_getInstanceViewPermissions: final permissions={permissions}")
return permissions
except Exception as e:
logger.debug(f"Error getting instance view permissions: {e}")
return permissions
return permissions # Fail-safe: no permissions on error
def _buildStaticBlocks(

View file

@ -173,7 +173,7 @@ class RbacClass:
try:
# Get Root mandate ID (first mandate in system)
allMandates = self.dbApp.getRecordset(Mandate)
rootMandateId = allMandates[0].get("id") if allMandates else None
rootMandateId = allMandates[0]["id"] if allMandates else None
# Collect mandates to check:
# - If mandateId provided: current mandate + Root mandate (if different)
@ -186,21 +186,21 @@ class RbacClass:
# Load roles from each mandate
for checkMandateId in mandatesToCheck:
userMandates = self.dbApp.getRecordset(
userMandateRecords = self.dbApp.getRecordset(
UserMandate,
recordFilter={"userId": user.id, "mandateId": checkMandateId, "enabled": True}
)
if userMandates:
userMandateId = userMandates[0].get("id")
if userMandateRecords:
userMandateId = userMandateRecords[0]["id"]
# Lade UserMandateRoles (Mandate-level roles)
userMandateRoles = self.dbApp.getRecordset(
userMandateRoleRecords = self.dbApp.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": userMandateId}
)
foundRoles = [r.get("roleId") for r in userMandateRoles if r.get("roleId")]
foundRoles = [r["roleId"] for r in userMandateRoleRecords if r.get("roleId")]
roleIds.update(foundRoles)
# Load FeatureAccess + FeatureAccessRole (Instance-level roles)
@ -215,14 +215,14 @@ class RbacClass:
)
if featureAccessRecords:
featureAccessId = featureAccessRecords[0].get("id")
featureAccessId = featureAccessRecords[0]["id"]
featureAccessRoles = self.dbApp.getRecordset(
featureAccessRoleRecords = self.dbApp.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
roleIds.update([r.get("roleId") for r in featureAccessRoles if r.get("roleId")])
roleIds.update([r["roleId"] for r in featureAccessRoleRecords if r.get("roleId")])
except Exception as e:
logger.error(f"Error loading role IDs for user {user.id}: {e}")
@ -377,12 +377,14 @@ class RbacClass:
if not roleRecords:
continue
role = roleRecords[0]
# Convert to Pydantic model for type-safe access
roleDict = {k: v for k, v in roleRecords[0].items() if not k.startswith("_")}
role = Role(**roleDict)
# Bestimme Priorität basierend auf Role-Scope
if role.get("featureInstanceId"):
if role.featureInstanceId:
priority = 3 # Instance-specific
elif role.get("mandateId"):
elif role.mandateId:
priority = 2 # Mandate-specific
else:
priority = 1 # Global

View file

@ -681,7 +681,7 @@ class ChatService:
"workflowId": workflow.id,
"process": process,
"engine": aiResponse.modelName,
"priceUsd": aiResponse.priceUsd,
"priceCHF": aiResponse.priceCHF,
"processingTime": aiResponse.processingTime,
"bytesSent": aiResponse.bytesSent,
"bytesReceived": aiResponse.bytesReceived,

View file

@ -39,7 +39,7 @@ class ExtractionService:
# Verify required internal model is available (used for pricing in extractContent)
modelDisplayName = "Internal Document Extractor"
model = modelRegistry.getModel(modelDisplayName)
if model is None or model.calculatePriceUsd is None:
if model is None or model.calculatepriceCHF is None:
raise RuntimeError(f"FATAL: Required internal model '{modelDisplayName}' is not available. Check connector registration.")
def extractContent(
@ -218,18 +218,18 @@ class ExtractionService:
modelDisplayName = "Internal Document Extractor"
model = modelRegistry.getModel(modelDisplayName)
# Hard fail if model is missing; caller must ensure connectors are registered
if model is None or model.calculatePriceUsd is None:
if model is None or model.calculatepriceCHF is None:
if docOperationId:
self.services.chat.progressLogFinish(docOperationId, False)
raise RuntimeError(f"Pricing model not available: {modelDisplayName}")
priceUsd = model.calculatePriceUsd(processingTime, bytesSent, bytesReceived)
priceCHF = model.calculatepriceCHF(processingTime, bytesSent, bytesReceived)
# Create AiCallResponse with real calculation
# Use model.name for the response (API identifier), not displayName
aiResponse = AiCallResponse(
content="", # No content for extraction stats needed
modelName=model.name,
priceUsd=priceUsd,
priceCHF=priceCHF,
processingTime=processingTime,
bytesSent=bytesSent,
bytesReceived=bytesReceived,
@ -478,7 +478,7 @@ class ExtractionService:
"resultSize": len(response.content),
"typeGroup": part.typeGroup,
"modelName": response.modelName,
"priceUsd": response.priceUsd
"priceCHF": response.priceCHF
}
)
@ -606,7 +606,7 @@ class ExtractionService:
"originalIndex": i, # Phase 7: Explicit order index
"processingOrder": i, # Phase 7: Processing order
"modelName": result.modelName,
"priceUsd": result.priceUsd,
"priceCHF": result.priceCHF,
"processingTime": result.processingTime,
"bytesSent": result.bytesSent,
"bytesReceived": result.bytesReceived
@ -1311,7 +1311,7 @@ class ExtractionService:
return AiCallResponse(
content=modelResponse.content,
modelName=model.name,
priceUsd=0.0,
priceCHF=0.0,
processingTime=processingTime,
bytesSent=0,
bytesReceived=0,
@ -1416,7 +1416,7 @@ class ExtractionService:
return AiCallResponse(
content=mergedContent,
modelName=model.name,
priceUsd=sum(r.priceUsd for r in chunkResults),
priceCHF=sum(r.priceCHF for r in chunkResults),
processingTime=sum(r.processingTime for r in chunkResults),
bytesSent=sum(r.bytesSent for r in chunkResults),
bytesReceived=sum(r.bytesReceived for r in chunkResults),
@ -1465,7 +1465,7 @@ class ExtractionService:
return AiCallResponse(
content=mergedContent,
modelName=model.name,
priceUsd=sum(r.priceUsd for r in chunkResults),
priceCHF=sum(r.priceCHF for r in chunkResults),
processingTime=sum(r.processingTime for r in chunkResults),
bytesSent=sum(r.bytesSent for r in chunkResults),
bytesReceived=sum(r.bytesReceived for r in chunkResults),
@ -1492,7 +1492,7 @@ class ExtractionService:
return AiCallResponse(
content=errorMsg,
modelName="error",
priceUsd=0.0,
priceCHF=0.0,
processingTime=0.0,
bytesSent=inputBytes,
bytesReceived=outputBytes,
@ -1622,7 +1622,7 @@ class ExtractionService:
return AiCallResponse(
content=mergedContent,
modelName="multiple",
priceUsd=sum(r.priceUsd for r in allResults),
priceCHF=sum(r.priceCHF for r in allResults),
processingTime=sum(r.processingTime for r in allResults),
bytesSent=sum(r.bytesSent for r in allResults),
bytesReceived=sum(r.bytesReceived for r in allResults),

View file

@ -576,22 +576,16 @@ def _deleteUserDataFromFeatureDatabases(userId: str, currentUser) -> Dict[str, A
rootInterface = getRootInterface()
# Get all feature accesses for this user
featureAccesses = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"userId": str(userId)}
)
# Get all feature accesses for this user using interface method
featureAccesses = rootInterface.getFeatureAccessesForUser(str(userId))
# Collect unique feature codes
featureCodes: Set[str] = set()
for fa in featureAccesses:
instanceId = fa.get("featureInstanceId")
instanceRecords = rootInterface.db.getRecordset(
FeatureInstance,
recordFilter={"id": instanceId}
)
if instanceRecords:
featureCode = instanceRecords[0].get("featureCode")
instanceId = fa.featureInstanceId
instance = rootInterface.getFeatureInstance(instanceId)
if instance:
featureCode = instance.featureCode
if featureCode:
featureCodes.add(featureCode)

View file

@ -25,11 +25,11 @@ FEATURE_ICON = "mdi-cog"
# Block Order (gemäss Navigation-API-Konzept):
# - System: 10
# - <dynamic/features>: 15 (wird in routeSystem.py eingefügt)
# - Workflows: 20
# - Basisdaten: 30
# - Migrate: 40
# - Administration: 200
#
# NOTE: Workflows and Migrate sections removed - now handled as features
#
# Item Order: Default-Abstand 10 pro Item
# uiComponent: Abgeleitet von objectKey (ui.system.home -> page.system.home)
# icon: Wird intern gehalten aber NICHT in der API Response zurückgegeben
@ -60,49 +60,6 @@ NAVIGATION_SECTIONS = [
},
],
},
{
"id": "workflows",
"title": {"en": "WORKFLOWS", "de": "WORKFLOWS", "fr": "WORKFLOWS"},
"order": 20,
"items": [
{
"id": "playground",
"objectKey": "ui.system.playground",
"label": {"en": "Chat Playground", "de": "Chat Playground", "fr": "Chat Playground"},
"icon": "FaPlay",
"path": "/workflows/playground",
"order": 10,
"public": True,
},
{
"id": "chats",
"objectKey": "ui.system.chats",
"label": {"en": "Chats", "de": "Chats", "fr": "Chats"},
"icon": "FaListAlt",
"path": "/workflows/list",
"order": 20,
"public": True,
},
{
"id": "automations",
"objectKey": "ui.system.automations",
"label": {"en": "Automations", "de": "Automatisierungen", "fr": "Automatisations"},
"icon": "FaCogs",
"path": "/workflows/automations",
"order": 30,
"public": True,
},
{
"id": "automation-templates",
"objectKey": "ui.system.automation-templates",
"label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
"icon": "FaFileAlt",
"path": "/workflows/automation-templates",
"order": 35,
"public": True,
},
],
},
{
"id": "basedata",
"title": {"en": "BASE DATA", "de": "BASISDATEN", "fr": "DONNÉES DE BASE"},
@ -134,54 +91,55 @@ NAVIGATION_SECTIONS = [
},
],
},
{
"id": "migrate",
"title": {"en": "MIGRATE TO FEATURES", "de": "MIGRATE TO FEATURES", "fr": "MIGRER VERS FEATURES"},
"order": 40,
"deprecated": True,
"items": [
{
"id": "chatbot",
"objectKey": "ui.system.chatbot",
"label": {"en": "Chatbot", "de": "Chatbot", "fr": "Chatbot"},
"icon": "FaComments",
"path": "/chatbot",
"order": 10,
"deprecated": True,
},
{
"id": "pek",
"objectKey": "ui.system.pek",
"label": {"en": "PEK", "de": "PEK", "fr": "PEK"},
"icon": "FaChartBar",
"path": "/pek",
"order": 20,
"deprecated": True,
},
{
"id": "speech",
"objectKey": "ui.system.speech",
"label": {"en": "Speech", "de": "Sprache", "fr": "Parole"},
"icon": "FaMicrophone",
"path": "/speech",
"order": 30,
"deprecated": True,
},
],
},
{
"id": "admin",
"title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"},
"order": 200,
"adminOnly": True,
"items": [
{
"id": "admin-users",
"objectKey": "ui.admin.users",
"label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
"icon": "FaUsers",
"path": "/admin/users",
"order": 10,
"adminOnly": True,
},
{
"id": "admin-invitations",
"objectKey": "ui.admin.invitations",
"label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"},
"icon": "FaEnvelopeOpenText",
"path": "/admin/invitations",
"order": 12,
"adminOnly": True,
},
{
"id": "admin-user-access-overview",
"objectKey": "ui.admin.userAccessOverview",
"label": {"en": "User Access Overview", "de": "Benutzer-Zugriffsübersicht", "fr": "Aperçu des accès utilisateur"},
"icon": "FaClipboardList",
"path": "/admin/user-access-overview",
"order": 14,
"adminOnly": True,
},
{
"id": "admin-mandates",
"objectKey": "ui.admin.mandates",
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
"icon": "FaBuilding",
"path": "/admin/mandates",
"order": 3,
"order": 20,
"adminOnly": True,
},
{
"id": "admin-user-mandates",
"objectKey": "ui.admin.userMandates",
"label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"},
"icon": "FaUserFriends",
"path": "/admin/user-mandates",
"order": 25,
"adminOnly": True,
},
{
@ -190,27 +148,36 @@ NAVIGATION_SECTIONS = [
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
"icon": "FaBuilding",
"path": "/admin/access",
"order": 5,
"adminOnly": True,
},
{
"id": "admin-users",
"objectKey": "ui.admin.users",
"label": {"en": "Users & Invitations", "de": "Benutzer & Einladungen", "fr": "Utilisateurs et invitations"},
"icon": "FaUsers",
"path": "/admin/users",
"order": 10,
"order": 30,
"adminOnly": True,
},
{
"id": "admin-roles",
"objectKey": "ui.admin.roles",
"label": {"en": "Roles & Permissions", "de": "Rollen & Berechtigungen", "fr": "Rôles et permissions"},
"icon": "FaKey",
"label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
"icon": "FaUserTag",
"path": "/admin/mandate-roles",
"order": 40,
"adminOnly": True,
},
{
"id": "admin-mandate-role-permissions",
"objectKey": "ui.admin.mandateRolePermissions",
"label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"},
"icon": "FaKey",
"path": "/admin/mandate-role-permissions",
"order": 45,
"adminOnly": True,
},
{
"id": "admin-feature-roles",
"objectKey": "ui.admin.featureRoles",
"label": {"en": "Feature Roles & Permissions", "de": "Features Rollen & Rechte", "fr": "Rôles et droits des features"},
"icon": "FaShieldAlt",
"path": "/admin/feature-roles",
"order": 50,
"adminOnly": True,
},
],
},
]

View file

@ -153,7 +153,7 @@ async def process(self, parameters: Dict[str, Any]) -> ActionResult:
metadata=AiResponseMetadata(
additionalData={
"modelName": aiResponse_obj.modelName,
"priceUsd": aiResponse_obj.priceUsd,
"priceCHF": aiResponse_obj.priceCHF,
"processingTime": aiResponse_obj.processingTime,
"bytesSent": aiResponse_obj.bytesSent,
"bytesReceived": aiResponse_obj.bytesReceived,

View file

@ -628,7 +628,7 @@ Width: {crawlWidth}
"hasContent": True,
"error": None,
"modelUsed": modelName,
"priceUsd": 0.0,
"priceCHF": 0.0,
"bytesSent": 0,
"bytesReceived": contentLength,
"isValidJson": True,