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 from modules.routes.routeMessaging import router as messagingRouter
app.include_router(messagingRouter) app.include_router(messagingRouter)
# Phase 8: New Feature Routes
from modules.routes.routeAdminFeatures import router as featuresAdminRouter from modules.routes.routeAdminFeatures import router as featuresAdminRouter
app.include_router(featuresAdminRouter) app.include_router(featuresAdminRouter)
@ -504,12 +503,6 @@ app.include_router(userAccessOverviewRouter)
from modules.routes.routeGdpr import router as gdprRouter from modules.routes.routeGdpr import router as gdprRouter
app.include_router(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.) # SYSTEM ROUTES (Navigation, etc.)
# ============================================================================ # ============================================================================

View file

@ -72,7 +72,7 @@ class AiAnthropic(BaseConnectorAi):
(OperationTypeEnum.DATA_EXTRACT, 8) (OperationTypeEnum.DATA_EXTRACT, 8)
), ),
version="claude-sonnet-4-5-20250929", 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( AiModel(
name="claude-sonnet-4-5-20250929", name="claude-sonnet-4-5-20250929",
@ -93,7 +93,7 @@ class AiAnthropic(BaseConnectorAi):
(OperationTypeEnum.IMAGE_ANALYSE, 10) (OperationTypeEnum.IMAGE_ANALYSE, 10)
), ),
version="claude-sonnet-4-5-20250929", 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, processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(), operationTypes=createOperationTypeRatings(),
version="internal-extractor-v1", 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( AiModel(
name="internal-generator", name="internal-generator",
@ -60,7 +60,7 @@ class AiInternal(BaseConnectorAi):
processingMode=ProcessingModeEnum.BASIC, processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(), operationTypes=createOperationTypeRatings(),
version="internal-generator-v1", 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( AiModel(
name="internal-renderer", name="internal-renderer",
@ -80,7 +80,7 @@ class AiInternal(BaseConnectorAi):
processingMode=ProcessingModeEnum.DETAILED, processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(), operationTypes=createOperationTypeRatings(),
version="internal-renderer-v1", 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) (OperationTypeEnum.DATA_EXTRACT, 7)
), ),
version="gpt-4o", 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( AiModel(
name="gpt-3.5-turbo", name="gpt-3.5-turbo",
@ -97,7 +97,7 @@ class AiOpenai(BaseConnectorAi):
# Note: GPT-3.5-turbo does NOT support vision/image operations # Note: GPT-3.5-turbo does NOT support vision/image operations
), ),
version="gpt-3.5-turbo", 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( AiModel(
name="gpt-4o", name="gpt-4o",
@ -118,7 +118,7 @@ class AiOpenai(BaseConnectorAi):
(OperationTypeEnum.IMAGE_ANALYSE, 9) (OperationTypeEnum.IMAGE_ANALYSE, 9)
), ),
version="gpt-4o", 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( AiModel(
name="dall-e-3", name="dall-e-3",
@ -140,7 +140,7 @@ class AiOpenai(BaseConnectorAi):
(OperationTypeEnum.IMAGE_GENERATE, 10) (OperationTypeEnum.IMAGE_GENERATE, 10)
), ),
version="dall-e-3", 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) (OperationTypeEnum.WEB_CRAWL, 7)
), ),
version="sonar", 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( AiModel(
name="sonar-pro", name="sonar-pro",
@ -97,7 +97,7 @@ class AiPerplexity(BaseConnectorAi):
(OperationTypeEnum.WEB_CRAWL, 8) (OperationTypeEnum.WEB_CRAWL, 8)
), ),
version="sonar-pro", 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) (OperationTypeEnum.WEB_CRAWL, 10)
), ),
version="tavily-search", 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) # Function reference (not serialized)
functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model") 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 # Selection criteria - capabilities with ratings
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Default priority for this model. See PriorityEnum for available values.") 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") content: str = Field(description="AI response content")
modelName: str = Field(description="Selected model name") 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") processingTime: float = Field(default=0.0, description="Duration in seconds")
bytesSent: int = Field(default=0, description="Input data size in bytes") bytesSent: int = Field(default=0, description="Input data size in bytes")
bytesReceived: int = Field(default=0, description="Output 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") 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')") 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')") 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( registerModelLabels(
@ -41,7 +41,7 @@ registerModelLabels(
"errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"}, "errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"},
"process": {"en": "Process", "fr": "Processus"}, "process": {"en": "Process", "fr": "Processus"},
"engine": {"en": "Engine", "fr": "Moteur"}, "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 # Template roles for this feature
# IMPORTANT: "viewer" role is required for automatic user assignment!
TEMPLATE_ROLES = [ 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", "roleLabel": "automation-admin",
"description": { "description": {
@ -161,9 +178,132 @@ def registerFeature(catalogService) -> bool:
meta=resObj.get("meta") 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") logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}") logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False 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): async def _getSharepointConnection(self, sharepointPath: str = None):
try: try:
connections = self.services.interfaceDbApp.db.getRecordset( # Use interface method to get user connections
UserConnection, connections = self.services.interfaceDbApp.getUserConnections(self.services.interfaceDbApp.userId)
recordFilter={"userId": self.services.interfaceDbApp.userId} msftConnections = [c for c in connections if c.authority == 'msft']
)
msftConnections = [c for c in connections if c.get('authority') == 'msft']
if not msftConnections: if not msftConnections:
logger.warning('No Microsoft connections found for user') logger.warning('No Microsoft connections found for user')
return None return None
if len(msftConnections) == 1: 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] return msftConnections[0]
if sharepointPath: if sharepointPath:
return await self._matchConnectionToPath(msftConnections, 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] return msftConnections[0]
except Exception: except Exception:
logger.error('Error getting SharePoint connection') logger.error('Error getting SharePoint connection')

View file

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

View file

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

View file

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

View file

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

View file

@ -72,6 +72,10 @@ def initBootstrap(db: DatabaseConnector) -> None:
# Seed automation templates (after admin user exists) # Seed automation templates (after admin user exists)
initAutomationTemplates(db, adminUserId) initAutomationTemplates(db, adminUserId)
# Initialize feature instances for root mandate
if mandateId:
initRootMandateFeatures(db, mandateId)
def initAutomationTemplates(dbApp: DatabaseConnector, adminUserId: Optional[str] = None) -> None: 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") 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]: def initRootMandate(db: DatabaseConnector) -> Optional[str]:
""" """
Creates the Root mandate if it doesn't exist. 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.datamodelFeatures import Feature, FeatureInstance
from modules.datamodels.datamodelInvitation import Invitation from modules.datamodels.datamodelInvitation import Invitation
from modules.datamodels.datamodelNotification import UserNotification
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -733,6 +734,9 @@ class AppObjects:
# Clear cache to ensure fresh data (already done above) # 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]) return User(**createdUser[0])
except ValueError as e: except ValueError as e:
@ -796,6 +800,99 @@ class AppObjects:
logger.error(f"Error updating user: {str(e)}") logger.error(f"Error updating user: {str(e)}")
raise ValueError(f"Failed to update 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: def disableUser(self, userId: str) -> User:
"""Disables a user if current user has permission.""" """Disables a user if current user has permission."""
return self.updateUser(userId, {"enabled": False}) return self.updateUser(userId, {"enabled": False})
@ -1209,6 +1306,31 @@ class AppObjects:
logger.error(f"Error getting user connections: {str(e)}") logger.error(f"Error getting user connections: {str(e)}")
return [] 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( def addUserConnection(
self, self,
userId: str, userId: str,
@ -1547,6 +1669,106 @@ class AppObjects:
logger.error(f"Error deleting UserMandate: {e}") logger.error(f"Error deleting UserMandate: {e}")
raise ValueError(f"Failed to delete 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]: def getRoleIdsForUserMandate(self, userMandateId: str) -> List[str]:
""" """
Get all role IDs assigned to a UserMandate. Get all role IDs assigned to a UserMandate.
@ -1688,6 +1910,30 @@ class AppObjects:
logger.error(f"Error getting FeatureAccesses: {e}") logger.error(f"Error getting FeatureAccesses: {e}")
return [] 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: def createFeatureAccess(self, userId: str, featureInstanceId: str, roleIds: List[str] = None) -> FeatureAccess:
""" """
Create a FeatureAccess record (grant user access to feature instance). 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}") logger.error(f"Error getting role IDs for FeatureAccess: {e}")
return [] 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 # Token methods
def saveAccessToken(self, token: Token, replace_existing: bool = True) -> None: def saveAccessToken(self, token: Token, replace_existing: bool = True) -> None:
@ -1908,6 +2593,56 @@ class AppObjects:
) )
return None 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( def findActiveTokenById(
self, self,
tokenId: str, tokenId: str,
@ -2340,6 +3075,42 @@ class AppObjects:
logger.error(f"Error getting role by label {roleLabel}: {str(e)}") logger.error(f"Error getting role by label {roleLabel}: {str(e)}")
return None 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]: def getAllRoles(self, pagination: Optional[PaginationParams] = None) -> Union[List[Role], PaginatedResult]:
""" """
Get all roles with optional pagination, sorting, and filtering. 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]: def _getUserRolesInInstance(rootInterface, userId: str, instanceId: str) -> List[str]:
"""Get all role labels for a user in a feature instance.""" """Get all role labels for a user in a feature instance."""
try: try:
from modules.datamodels.datamodelRbac import Role # Get FeatureAccess for this user and instance (Pydantic model)
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
# Get FeatureAccess for this user and instance if featureAccess:
featureAccesses = rootInterface.db.getRecordset( # Get role IDs via interface method
FeatureAccess, roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
if featureAccesses:
featureAccessId = featureAccesses[0].get("id")
# Get role IDs via FeatureAccessRole junction table if roleIds:
featureAccessRoles = rootInterface.db.getRecordset( # Get ALL roles and extract labels
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
if featureAccessRoles:
# Get ALL roles, not just the first one
roleLabels = [] roleLabels = []
for far in featureAccessRoles: for roleId in roleIds:
roleId = far.get("roleId") role = rootInterface.getRole(roleId)
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId}) if role:
if roles: roleLabels.append(role.roleLabel)
roleLabels.append(roles[0].get("roleLabel", "user"))
return roleLabels if roleLabels else ["user"] return roleLabels if roleLabels else ["user"]
return ["user"] # Default return ["user"] # Default - no access means basic user level
except Exception as e: except Exception as e:
logger.debug(f"Error getting user roles: {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]: def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict[str, Any]:
@ -249,66 +237,53 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
} }
try: try:
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext, Role from modules.datamodels.datamodelRbac import AccessRuleContext
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
# Get FeatureAccess for this user and instance # Get FeatureAccess for this user and instance (Pydantic model)
featureAccesses = rootInterface.db.getRecordset( featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
FeatureAccess,
recordFilter={"userId": userId, "featureInstanceId": 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}") logger.debug(f"_getInstancePermissions: No FeatureAccess found for user {userId} and instance {instanceId}")
return permissions return permissions
# Get role IDs via FeatureAccessRole junction table # Get role IDs via interface method
featureAccessId = featureAccesses[0].get("id") roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
featureAccessRoles = rootInterface.db.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
roleIds = [far.get("roleId") for far in featureAccessRoles]
logger.debug(f"_getInstancePermissions: featureAccessId={featureAccessId}, roleIds={roleIds}") logger.debug(f"_getInstancePermissions: featureAccessId={featureAccess.id}, roleIds={roleIds}")
if not 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 return permissions
# Check if user has admin role # Check if user has admin role
for roleId in roleIds: for roleId in roleIds:
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId}) role = rootInterface.getRole(roleId)
if roles: if role and "admin" in role.roleLabel.lower():
roleLabel = roles[0].get("roleLabel", "").lower() permissions["isAdmin"] = True
if "admin" in roleLabel: break
permissions["isAdmin"] = True
break
# Get permissions (AccessRules) for all roles # Get permissions (AccessRules) for all roles
for roleId in roleIds: for roleId in roleIds:
accessRules = rootInterface.db.getRecordset( # Get all rules for this role (returns Pydantic models)
AccessRule, accessRules = rootInterface.getAccessRules(roleId=roleId)
recordFilter={"roleId": roleId}
)
logger.debug(f"_getInstancePermissions: roleId={roleId}, accessRules={len(accessRules) if accessRules else 0}") logger.debug(f"_getInstancePermissions: roleId={roleId}, accessRules={len(accessRules) if accessRules else 0}")
for rule in accessRules: for rule in accessRules:
context = rule.get("context", "") context = rule.context
item = rule.get("item", "") item = rule.item or ""
# Handle DATA context (tables/fields) # Handle DATA context (tables/fields)
if context == "DATA" or context == AccessRuleContext.DATA: if context == AccessRuleContext.DATA or context == "DATA":
if item: if item:
# Check if it's a field (table.field) or table # Check if it's a field (table.field) or table
if "." in item: if "." in item:
tableName, fieldName = item.split(".", 1) tableName, fieldName = item.split(".", 1)
if fieldName not in permissions["fields"]: if fieldName not in permissions["fields"]:
permissions["fields"][fieldName] = {"view": False} 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: else:
tableName = item tableName = item
if tableName not in permissions["tables"]: if tableName not in permissions["tables"]:
@ -322,20 +297,18 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
# Merge permissions (highest wins) # Merge permissions (highest wins)
current = permissions["tables"][tableName] current = permissions["tables"][tableName]
current["view"] = current["view"] or rule.get("view", False) current["view"] = current["view"] or rule.view
current["read"] = _mergeAccessLevel(current["read"], rule.get("read") or "n") current["read"] = _mergeAccessLevel(current["read"], rule.read or "n")
current["create"] = _mergeAccessLevel(current["create"], rule.get("create") or "n") current["create"] = _mergeAccessLevel(current["create"], rule.create or "n")
current["update"] = _mergeAccessLevel(current["update"], rule.get("update") or "n") current["update"] = _mergeAccessLevel(current["update"], rule.update or "n")
current["delete"] = _mergeAccessLevel(current["delete"], rule.get("delete") or "n") current["delete"] = _mergeAccessLevel(current["delete"], rule.delete or "n")
# Handle UI context (views) # Handle UI context (views)
# Views are stored with full objectKey (e.g., ui.feature.trustee.dashboard) elif context == AccessRuleContext.UI or context == "UI":
elif context == "UI" or context == AccessRuleContext.UI:
ruleView = rule.get("view", False)
if item: if item:
# Store with full objectKey as per Navigation-API-Konzept # Store with full objectKey as per Navigation-API-Konzept
permissions["views"][item] = permissions["views"].get(item, False) or ruleView permissions["views"][item] = permissions["views"].get(item, False) or rule.view
elif ruleView: elif rule.view:
# item=None means all views - set a wildcard flag # item=None means all views - set a wildcard flag
permissions["views"]["_all"] = True permissions["views"]["_all"] = True
@ -343,7 +316,7 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
except Exception as e: except Exception as e:
logger.debug(f"Error getting instance permissions: {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: def _mergeAccessLevel(current: str, new: str) -> str:
@ -924,49 +897,35 @@ async def list_feature_instance_users(
detail="Access denied to this feature instance" detail="Access denied to this feature instance"
) )
# Get all FeatureAccess records for this instance # Get all FeatureAccess records for this instance (Pydantic models)
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole featureAccesses = rootInterface.getFeatureAccessesByInstance(instanceId)
from modules.datamodels.datamodelRbac import Role
featureAccesses = rootInterface.db.getRecordset(
FeatureAccess,
recordFilter={"featureInstanceId": instanceId}
)
result = [] result = []
for fa in featureAccesses: for fa in featureAccesses:
userId = fa.get("userId") # Get user info (Pydantic model)
featureAccessId = fa.get("id") user = rootInterface.getUser(str(fa.userId))
if not user:
# Get user info
users = rootInterface.db.getRecordset(UserInDB, recordFilter={"id": userId})
if not users:
continue continue
user = users[0]
# Get role IDs via FeatureAccessRole junction table # Get role IDs via interface method
featureAccessRoles = rootInterface.db.getRecordset( roleIds = rootInterface.getRoleIdsForFeatureAccess(str(fa.id))
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
roleIds = [far.get("roleId") for far in featureAccessRoles]
# Get role labels # Get role labels
roleLabels = [] roleLabels = []
for roleId in roleIds: for roleId in roleIds:
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId}) role = rootInterface.getRole(roleId)
if roles: if role:
roleLabels.append(roles[0].get("roleLabel", "")) roleLabels.append(role.roleLabel)
result.append(FeatureInstanceUserResponse( result.append(FeatureInstanceUserResponse(
id=featureAccessId, # FeatureAccess ID as primary key id=str(fa.id), # FeatureAccess ID as primary key
userId=userId, userId=str(fa.userId),
username=user.get("username", ""), username=user.username,
email=user.get("email"), email=user.email,
fullName=user.get("fullName"), fullName=user.fullName,
roleIds=roleIds, roleIds=roleIds,
roleLabels=roleLabels, roleLabels=roleLabels,
enabled=fa.get("enabled", True) enabled=fa.enabled
)) ))
return result return result
@ -1026,8 +985,8 @@ async def add_user_to_feature_instance(
) )
# Verify user exists # Verify user exists
users = rootInterface.db.getRecordset(UserInDB, recordFilter={"id": data.userId}) user = rootInterface.getUser(data.userId)
if not users: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"User '{data.userId}' 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 # Check if user already has access
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
existingAccess = rootInterface.db.getRecordset( existingAccess = rootInterface.getFeatureAccess(data.userId, instanceId)
FeatureAccess,
recordFilter={"userId": data.userId, "featureInstanceId": instanceId}
)
if existingAccess: if existingAccess:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=status.HTTP_409_CONFLICT,
@ -1131,17 +1087,14 @@ async def remove_user_from_feature_instance(
# Find FeatureAccess record # Find FeatureAccess record
from modules.datamodels.datamodelMembership import FeatureAccess from modules.datamodels.datamodelMembership import FeatureAccess
existingAccess = rootInterface.db.getRecordset( existingAccess = rootInterface.getFeatureAccess(userId, instanceId)
FeatureAccess,
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
if not existingAccess: if not existingAccess:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="User does not have access to this feature instance" 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) # Delete FeatureAccess (CASCADE will delete FeatureAccessRole records)
rootInterface.db.recordDelete(FeatureAccess, featureAccessId) rootInterface.db.recordDelete(FeatureAccess, featureAccessId)
@ -1215,29 +1168,21 @@ async def update_feature_instance_user_roles(
# Find FeatureAccess record # Find FeatureAccess record
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
existingAccess = rootInterface.db.getRecordset( existingAccess = rootInterface.getFeatureAccess(userId, instanceId)
FeatureAccess,
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
if not existingAccess: if not existingAccess:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="User does not have access to this feature instance" detail="User does not have access to this feature instance"
) )
featureAccessId = existingAccess[0].get("id") featureAccessId = str(existingAccess.id)
# Update enabled flag if provided # Update enabled flag if provided
if data.enabled is not None: if data.enabled is not None:
rootInterface.db.recordModify(FeatureAccess, featureAccessId, {"enabled": data.enabled}) rootInterface.db.recordModify(FeatureAccess, featureAccessId, {"enabled": data.enabled})
# Delete existing FeatureAccessRole records # Delete existing FeatureAccessRole records via interface method
existingRoles = rootInterface.db.getRecordset( rootInterface.deleteFeatureAccessRoles(featureAccessId)
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
for role in existingRoles:
rootInterface.db.recordDelete(FeatureAccessRole, role.get("id"))
# Create new FeatureAccessRole records # Create new FeatureAccessRole records
for roleId in data.roleIds: for roleId in data.roleIds:
@ -1304,21 +1249,17 @@ async def get_feature_instance_available_roles(
detail="Access denied to this feature instance" detail="Access denied to this feature instance"
) )
# Get roles for this instance # Get roles for this instance using interface method
from modules.datamodels.datamodelRbac import Role instanceRoles = rootInterface.getRolesByFeatureInstance(instanceId)
instanceRoles = rootInterface.db.getRecordset(
Role,
recordFilter={"featureInstanceId": instanceId}
)
result = [] result = []
for role in instanceRoles: for role in instanceRoles:
result.append({ result.append({
"id": role.get("id"), "id": role.id,
"roleLabel": role.get("roleLabel"), "roleLabel": role.roleLabel,
"description": role.get("description", {}), "description": role.description or {},
"featureCode": role.get("featureCode"), "featureCode": role.featureCode,
"isSystemRole": role.get("isSystemRole", False) "isSystemRole": role.isSystemRole
}) })
return result return result
@ -1394,15 +1335,13 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
# Check if any of the user's roles is an admin role # Check if any of the user's roles is an admin role
try: try:
rootInterface = getRootInterface() rootInterface = getRootInterface()
from modules.datamodels.datamodelRbac import Role
for roleId in context.roleIds: for roleId in context.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId}) role = rootInterface.getRole(roleId)
if roleRecords: if role:
role = roleRecords[0] roleLabel = role.roleLabel
roleLabel = role.get("roleLabel", "")
# Admin role at mandate level (not feature-instance level) # 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 True
return False return False

View file

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

View file

@ -36,25 +36,17 @@ def _getUserRoleLabels(interface, userId: str) -> List[str]:
""" """
roleLabels: Set[str] = set() roleLabels: Set[str] = set()
# Get all UserMandate records for this user # Get all UserMandate records for this user (Pydantic models)
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId}) userMandates = interface.getUserMandates(userId)
for um in userMandates: for um in userMandates:
userMandateId = um.get("id") # Get all UserMandateRole records for this membership (Pydantic models)
if not userMandateId: userMandateRoles = interface.getUserMandateRoles(str(um.id))
continue
# Get all UserMandateRole records for this membership
userMandateRoles = interface.db.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": str(userMandateId)}
)
for umr in userMandateRoles: for umr in userMandateRoles:
roleId = umr.get("roleId") if umr.roleId:
if roleId:
# Get role by ID to get roleLabel # Get role by ID to get roleLabel
role = interface.getRole(str(roleId)) role = interface.getRole(str(umr.roleId))
if role: if role:
roleLabels.add(role.roleLabel) roleLabels.add(role.roleLabel)
@ -362,21 +354,13 @@ async def list_users_with_roles(
try: try:
interface = getRootInterface() interface = getRootInterface()
# Get all users (SysAdmin sees all) # Get all users via interface method (Pydantic models)
# Use db.getRecordset with UserInDB (the actual database model) users = interface.getAllUsers()
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))
# Filter by mandate if specified (via UserMandate table) # Filter by mandate if specified (via UserMandate table)
if mandateId: if mandateId:
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId}) userMandates = interface.getUserMandatesByMandate(mandateId)
mandateUserIds = {str(um["userId"]) for um in userMandates} mandateUserIds = {str(um.userId) for um in userMandates}
users = [u for u in users if str(u.id) in mandateUserIds] users = [u for u in users if str(u.id) in mandateUserIds]
# Filter by role if specified (via UserMandateRole) # 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}") logger.warning(f"Non-standard role label assigned: {roleLabel}")
# Get user's first mandate (for role assignment) # Get user's first mandate (for role assignment)
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId}) userMandates = interface.getUserMandates(userId)
if not userMandates: if not userMandates:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail=f"User {userId} has no mandate memberships. Add to mandate first." 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 # Get current roles for this mandate (Pydantic models)
existingRoles = interface.db.getRecordset( existingRoles = interface.getUserMandateRoles(userMandateId)
UserMandateRole, existingRoleIds = {str(r.roleId) for r in existingRoles}
recordFilter={"userMandateId": userMandateId}
)
existingRoleIds = {str(r.get("roleId")) for r in existingRoles}
# Convert roleLabels to roleIds # Convert roleLabels to roleIds
newRoleIds = set() newRoleIds = set()
@ -524,8 +505,8 @@ async def update_user_roles(
# Remove roles that are no longer needed # Remove roles that are no longer needed
for existingRole in existingRoles: for existingRole in existingRoles:
if str(existingRole.get("roleId")) not in newRoleIds: if str(existingRole.roleId) not in newRoleIds:
interface.db.recordDelete(UserMandateRole, str(existingRole.get("id"))) interface.removeRoleFromUserMandate(userMandateId, str(existingRole.roleId))
# Add new roles # Add new roles
for roleId in newRoleIds: for roleId in newRoleIds:
@ -596,25 +577,22 @@ async def add_user_role(
) )
# Get user's first mandate # Get user's first mandate
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId}) userMandates = interface.getUserMandates(userId)
if not userMandates: if not userMandates:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail=f"User {userId} has no mandate memberships. Add to mandate first." 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 # Check if role is already assigned - use interface method
existingAssignment = interface.db.getRecordset( existingRoles = interface.getUserMandateRoles(userMandateId)
UserMandateRole, roleAlreadyAssigned = any(str(r.roleId) == str(role.id) for r in existingRoles)
recordFilter={"userMandateId": userMandateId, "roleId": str(role.id)}
)
if not existingAssignment: if not roleAlreadyAssigned:
# Add the role # Add the role via interface method
newRole = UserMandateRole(userMandateId=userMandateId, roleId=str(role.id)) interface.addRoleToUserMandate(userMandateId, str(role.id))
interface.db.recordCreate(UserMandateRole, newRole.model_dump())
logger.info(f"Added role {roleLabel} to user {userId} by SysAdmin {currentUser.id}") logger.info(f"Added role {roleLabel} to user {userId} by SysAdmin {currentUser.id}")
userRoleLabels = _getUserRoleLabels(interface, userId) userRoleLabels = _getUserRoleLabels(interface, userId)
@ -678,20 +656,14 @@ async def remove_user_role(
) )
# Remove role from all user's mandates # Remove role from all user's mandates
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId}) userMandates = interface.getUserMandates(userId)
roleRemoved = False roleRemoved = False
for um in userMandates: for um in userMandates:
userMandateId = str(um.get("id")) userMandateId = str(um.id)
# Find and delete the role assignment # Remove role via interface method
assignments = interface.db.getRecordset( if interface.removeRoleFromUserMandate(userMandateId, str(role.id)):
UserMandateRole,
recordFilter={"userMandateId": userMandateId, "roleId": str(role.id)}
)
for assignment in assignments:
interface.db.recordDelete(UserMandateRole, str(assignment.get("id")))
roleRemoved = True roleRemoved = True
if roleRemoved: if roleRemoved:
@ -751,25 +723,21 @@ async def get_users_with_role(
detail=f"Role '{roleLabel}' not found" detail=f"Role '{roleLabel}' not found"
) )
# Get all UserMandateRole assignments for this role # Get all UserMandateRole assignments for this role (Pydantic models)
roleAssignments = interface.db.getRecordset( roleAssignments = interface.getUserMandateRolesByRole(str(role.id))
UserMandateRole,
recordFilter={"roleId": str(role.id)}
)
# Get unique userMandateIds # 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 # Get userIds from UserMandate records
userIds: Set[str] = set() userIds: Set[str] = set()
for userMandateId in userMandateIds: for userMandateId in userMandateIds:
umRecords = interface.db.getRecordset(UserMandate, recordFilter={"id": userMandateId}) um = interface.getUserMandateById(userMandateId)
if umRecords: if um:
um = umRecords[0]
# Filter by mandate if specified # Filter by mandate if specified
if mandateId and str(um.get("mandateId")) != mandateId: if mandateId and str(um.mandateId) != mandateId:
continue continue
userIds.add(str(um.get("userId"))) userIds.add(str(um.userId))
# Get users and format response # Get users and format response
result = [] 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 # 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 # This allows users to access system UI elements without needing a specific mandate header
userMandates = rootInterface.db.getRecordset( allUserMandates = rootInterface.getUserMandates(str(reqContext.user.id))
UserMandate, userMandates = [um for um in allUserMandates if um.enabled]
recordFilter={"userId": str(reqContext.user.id), "enabled": True}
)
logger.debug(f"UI/RESOURCE permissions: Found {len(userMandates)} UserMandates for user {reqContext.user.id}") 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 # Collect all role IDs the user has across all mandates
for userMandate in userMandates: for userMandate in userMandates:
mandateRoleIds = rootInterface.getRoleIdsForUserMandate(userMandate.get("id")) mandateRoleIds = rootInterface.getRoleIdsForUserMandate(userMandate.id)
logger.debug(f"UI/RESOURCE permissions: UserMandate {userMandate.get('id')} (mandate {userMandate.get('mandateId')}) has {len(mandateRoleIds)} roles: {mandateRoleIds}") logger.debug(f"UI/RESOURCE permissions: UserMandate {userMandate.id} (mandate {userMandate.mandateId}) has {len(mandateRoleIds)} roles: {mandateRoleIds}")
for rid in mandateRoleIds: for rid in mandateRoleIds:
if rid not in roleIds: if rid not in roleIds:
roleIds.append(rid) roleIds.append(rid)
@ -210,14 +208,11 @@ async def get_all_permissions(
allRules[ctx] = [] allRules[ctx] = []
# Get all rules for user's roles - bypass RBAC filtering # Get all rules for user's roles - bypass RBAC filtering
for roleId in roleIds: for roleId in roleIds:
ruleRecords = rootInterface.db.getRecordset( # Use interface method and filter by context
AccessRule, rules = rootInterface.getAccessRulesByRole(str(roleId))
recordFilter={"roleId": str(roleId), "context": ctx.value} for rule in rules:
) if rule.context == ctx.value:
for ruleRecord in ruleRecords: allRules[ctx].append(rule)
# Convert dict to AccessRule object
cleanedRule = {k: v for k, v in ruleRecord.items() if not k.startswith("_")}
allRules[ctx].append(AccessRule(**cleanedRule))
# Build result: for each context, collect all unique items and calculate permissions # Build result: for each context, collect all unique items and calculate permissions
for ctx in contextsToFetch: for ctx in contextsToFetch:
@ -405,14 +400,8 @@ async def get_access_rules_by_role(
try: try:
interface = getRootInterface() interface = getRootInterface()
# Build filter for roleId # Get rules from database using interface method
recordFilter = {"roleId": roleId} ruleObjects = interface.getAccessRulesByRole(roleId)
# Get rules from database
rules = interface.db.getRecordset(AccessRule, recordFilter=recordFilter)
# Convert to AccessRule objects
ruleObjects = [AccessRule(**rule) for rule in rules]
return PaginatedResponse( return PaginatedResponse(
items=[rule.model_dump() for rule in ruleObjects], items=[rule.model_dump() for rule in ruleObjects],
@ -1128,13 +1117,9 @@ async def getCatalogObjects(
if mandateId: if mandateId:
try: try:
interface = getRootInterface() interface = getRootInterface()
# Get all feature instances for this mandate # Get all feature instances for this mandate using interface method
from modules.datamodels.datamodelFeatures import FeatureInstance instances = interface.getFeatureInstancesByMandate(mandateId, enabledOnly=True)
instances = interface.db.getRecordset( activeFeatures = set(inst.featureCode for inst in instances)
FeatureInstance,
recordFilter={"mandateId": mandateId, "enabled": True}
)
activeFeatures = set(inst.get("featureCode") for inst in instances)
# Always include "system" feature # Always include "system" feature
activeFeatures.add("system") activeFeatures.add("system")
except Exception as e: except Exception as e:

View file

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

View file

@ -291,9 +291,9 @@ async def delete_mandate(
) )
# MULTI-TENANT: Delete all UserMandate entries for this mandate first # 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: 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}") logger.info(f"Deleted {len(userMandates)} UserMandate entries for mandate {mandateId}")
# Delete mandate # Delete mandate
@ -377,39 +377,46 @@ async def list_mandate_users(
) )
# Get all UserMandate entries for this mandate # Get all UserMandate entries for this mandate
userMandates = rootInterface.db.getRecordset( userMandates = rootInterface.getUserMandatesByMandate(targetMandateId)
UserMandate,
recordFilter={"mandateId": targetMandateId}
)
result = [] result = []
for um in userMandates: for um in userMandates:
# Get user info # Get user info
user = rootInterface.getUser(um.get("userId")) user = rootInterface.getUser(str(um.userId))
if not user: if not user:
continue continue
# Get roles for this membership # 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 = [] roleLabels = []
filteredRoleIds = []
seenLabels = set()
for roleId in roleIds: for roleId in roleIds:
role = rootInterface.getRole(roleId) role = rootInterface.getRole(roleId)
if role: if role:
roleLabels.append(role.roleLabel) # 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: 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({ result.append({
"id": um.get("id"), # UserMandate ID as primary key "id": str(um.id), # UserMandate ID as primary key
"userId": str(user.id), "userId": str(user.id),
"username": user.username, "username": user.username,
"email": user.email, "email": user.email,
"fullName": user.fullName, "fullName": user.fullName,
"roleIds": roleIds, "roleIds": filteredRoleIds,
"roleLabels": roleLabels, "roleLabels": roleLabels,
"enabled": um.get("enabled", True) "enabled": um.enabled
}) })
# Apply search, filtering, and sorting if pagination requested # 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) # 6. Validate roles (must exist and belong to this mandate or be global)
for roleId in data.roleIds: for roleId in data.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId}) try:
if not roleRecords: rootInterface.validateRoleForMandate(roleId, targetMandateId)
raise HTTPException( except ValueError as e:
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):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Role {roleId} belongs to a different mandate" detail=str(e)
) )
# 7. Create UserMandate # 7. Create UserMandate
@ -718,18 +719,12 @@ async def update_user_roles_in_mandate(
# Validate new roles # Validate new roles
for roleId in roleIds: for roleId in roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId}) try:
if not roleRecords: rootInterface.validateRoleForMandate(roleId, targetMandateId)
raise HTTPException( except ValueError as e:
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):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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 # 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 # Remove existing role assignments
existingRoles = rootInterface.db.getRecordset( rootInterface.deleteUserMandateRoles(str(membership.id))
UserMandateRole,
recordFilter={"userMandateId": str(membership.id)}
)
for er in existingRoles:
rootInterface.db.recordDelete(UserMandateRole, er.get("id"))
# Add new role assignments # Add new role assignments
for roleId in roleIds: for roleId in roleIds:
@ -812,19 +802,17 @@ def _hasMandateAdminRole(context: RequestContext, mandateId: str) -> bool:
rootInterface = interfaceDbApp.getRootInterface() rootInterface = interfaceDbApp.getRootInterface()
for roleId in context.roleIds: for roleId in context.roleIds:
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId}) role = rootInterface.getRole(roleId)
if roleRecords: if role:
role = roleRecords[0]
roleLabel = role.get("roleLabel", "")
# Admin role at mandate level (not feature-instance level) # 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 True
return False return False
except Exception as e: except Exception as e:
logger.error(f"Error checking mandate admin role: {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: 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. Check if excluding this user would leave the mandate without any admins.
""" """
try: try:
# Get all UserMandates for this mandate # Get all UserMandates for this mandate (Pydantic models)
userMandates = interface.db.getRecordset( allMandates = interface.getUserMandatesByMandate(mandateId)
UserMandate, userMandates = [um for um in allMandates if um.enabled]
recordFilter={"mandateId": mandateId, "enabled": True}
)
adminCount = 0 adminCount = 0
for um in userMandates: for um in userMandates:
if str(um.get("userId")) == str(excludeUserId): if str(um.userId) == str(excludeUserId):
continue continue
# Check if this user has admin role # Check if this user has admin role
roleIds = interface.getRoleIdsForUserMandate(um.get("id")) roleIds = interface.getRoleIdsForUserMandate(str(um.id))
if _hasAdminRoleInList(interface, roleIds, mandateId): if _hasAdminRoleInList(interface, roleIds, mandateId):
adminCount += 1 adminCount += 1
@ -852,7 +838,7 @@ def _isLastMandateAdmin(interface, mandateId: str, excludeUserId: str) -> bool:
except Exception as e: except Exception as e:
logger.error(f"Error checking last admin: {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: 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. Check if any of the role IDs is an admin role for the mandate.
""" """
for roleId in roleIds: for roleId in roleIds:
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId}) role = interface.getRole(roleId)
if roleRecords: if role:
role = roleRecords[0] # Admin role at mandate level (global or mandate-specific, not feature-instance)
roleLabel = role.get("roleLabel", "") if role.roleLabel == "admin" and not role.featureInstanceId:
roleMandateId = role.get("mandateId") if not role.mandateId or str(role.mandateId) == str(mandateId):
# Admin role at mandate level
if roleLabel == "admin" and (not roleMandateId or str(roleMandateId) == str(mandateId)):
if not role.get("featureInstanceId"):
return True return True
return False return False

View file

@ -21,7 +21,8 @@ import modules.interfaces.interfaceDbApp as interfaceDbApp
from modules.auth import limiter, getRequestContext, RequestContext from modules.auth import limiter, getRequestContext, RequestContext
# Import the attribute definition and helper functions # 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 from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
# Configure logger # Configure logger
@ -251,16 +252,10 @@ async def get_users(
) )
elif context.isSysAdmin: elif context.isSysAdmin:
# SysAdmin without mandateId sees all users # SysAdmin without mandateId sees all users
# Get all users directly from database using UserInDB (the actual database model) # Get all users via interface method (returns Pydantic User models)
allUsers = appInterface.db.getRecordset(UserInDB) allUserModels = appInterface.getAllUsers()
# Convert to cleaned dictionaries first for filtering # Convert to dictionaries for filtering/sorting
cleanedUsers = [] cleanedUsers = [u.model_dump() for u in allUserModels]
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)
# Apply server-side filtering and sorting # Apply server-side filtering and sorting
filteredUsers = _applyFiltersAndSort(cleanedUsers, paginationParams) filteredUsers = _applyFiltersAndSort(cleanedUsers, paginationParams)
@ -331,11 +326,7 @@ async def get_user(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin) # MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin: if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
if not userMandate: if not userMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, 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) # MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin: if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
if not userMandate: if not userMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, 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) # MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin: if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
if not userMandate: if not userMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, 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) # MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin: if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
if not userMandate: if not userMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, 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) # MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin: if context.mandateId and not context.isSysAdmin:
from modules.datamodels.datamodelMembership import UserMandate userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
"userId": userId,
"mandateId": str(context.mandateId)
})
if not userMandate: if not userMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
@ -803,10 +778,9 @@ async def delete_user(
) )
# Delete UserMandate entries for this user first # Delete UserMandate entries for this user first
from modules.datamodels.datamodelMembership import UserMandate userMandates = appInterface.getUserMandates(userId)
userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
for um in userMandates: for um in userMandates:
appInterface.db.deleteRecord(UserMandate, um["id"]) appInterface.deleteUserMandate(userId, str(um.mandateId))
success = appInterface.deleteUser(userId) success = appInterface.deleteUser(userId)
if not success: if not success:

View file

@ -163,16 +163,14 @@ async def update_workflow(
# Get workflow interface with current user context # Get workflow interface with current user context
workflowInterface = getInterface(currentUser) workflowInterface = getInterface(currentUser)
# Get raw workflow data from database to check permissions # Get workflow using interface method to check permissions
workflows = workflowInterface.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId}) workflow = workflowInterface.getWorkflow(workflowId)
if not workflows: if not workflow:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Workflow not found" detail="Workflow not found"
) )
workflow_data = workflows[0]
# Check if user has permission to update using RBAC # Check if user has permission to update using RBAC
if not workflowInterface.checkRbacPermission(ChatWorkflow, "update", workflowId): if not workflowInterface.checkRbacPermission(ChatWorkflow, "update", workflowId):
raise HTTPException( raise HTTPException(
@ -230,6 +228,49 @@ async def get_workflow_status(
detail=f"Error getting workflow status: {str(e)}" 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 # API Endpoint for workflow logs with selective data transfer
@router.get("/{workflowId}/logs", response_model=PaginatedResponse[ChatLog]) @router.get("/{workflowId}/logs", response_model=PaginatedResponse[ChatLog])
@limiter.limit("120/minute") @limiter.limit("120/minute")

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ import httpx
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection 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 import createAccessToken, setAccessTokenCookie, createRefreshToken, setRefreshTokenCookie
from modules.auth.tokenManager import TokenManager from modules.auth.tokenManager import TokenManager
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
@ -171,10 +171,9 @@ async def login(
try: try:
if connectionId: if connectionId:
rootInterface = getRootInterface() rootInterface = getRootInterface()
records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId}) connection = rootInterface.getUserConnectionById(connectionId)
if records: if connection:
record = records[0] login_hint = connection.externalEmail or connection.externalUsername
login_hint = record.get("externalEmail") or record.get("externalUsername")
if login_hint: if login_hint:
extra_params["login_hint"] = login_hint extra_params["login_hint"] = login_hint
if "@" in login_hint: if "@" in login_hint:
@ -260,23 +259,20 @@ async def auth_callback(code: str, state: str, request: Request, response: Respo
rootInterface = getRootInterface() rootInterface = getRootInterface()
# Prefer connection flow reuse; fallback to user access token # Prefer connection flow reuse; fallback to user access token
if connection_id: if connection_id:
existing_tokens = rootInterface.db.getRecordset(Token, recordFilter={ existing_tokens = rootInterface.getTokensByConnectionIdAndAuthority(
"connectionId": connection_id, connection_id, AuthAuthority.GOOGLE
"authority": AuthAuthority.GOOGLE )
})
if existing_tokens: if existing_tokens:
# Use most recent by createdAt # Use most recent by createdAt
existing_tokens.sort(key=lambda x: parseTimestamp(x.get("createdAt"), default=0), reverse=True) existing_tokens.sort(key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True)
token_response["refresh_token"] = existing_tokens[0].get("tokenRefresh", "") token_response["refresh_token"] = existing_tokens[0].tokenRefresh or ""
if not token_response.get("refresh_token") and user_id: if not token_response.get("refresh_token") and user_id:
existing_access_tokens = rootInterface.db.getRecordset(Token, recordFilter={ existing_access_tokens = rootInterface.getTokensByUserIdNoConnection(
"userId": user_id, user_id, AuthAuthority.GOOGLE
"connectionId": None, )
"authority": AuthAuthority.GOOGLE
})
if existing_access_tokens: if existing_access_tokens:
existing_access_tokens.sort(key=lambda x: parseTimestamp(x.get("createdAt"), default=0), reverse=True) existing_access_tokens.sort(key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True)
token_response["refresh_token"] = existing_access_tokens[0].get("tokenRefresh", "") token_response["refresh_token"] = existing_access_tokens[0].tokenRefresh or ""
except Exception: except Exception:
# Non-fatal; continue without refresh token # Non-fatal; continue without refresh token
pass 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 from modules.datamodels.datamodelUam import Mandate
currentTime = getUtcTimestamp() currentTime = getUtcTimestamp()
pendingInvitations = appInterface.db.getRecordset( pendingInvitations = appInterface.getInvitationsByTargetUsername(userData.username)
model_class=Invitation,
recordFilter={"targetUsername": userData.username}
)
for invitation in pendingInvitations: for invitation in pendingInvitations:
# Skip expired, revoked, or fully used invitations # Skip expired, revoked, or fully used invitations
if invitation.get("expiresAt", 0) < currentTime: if (invitation.expiresAt or 0) < currentTime:
continue continue
if invitation.get("revokedAt"): if invitation.revokedAt:
continue continue
if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1): if (invitation.currentUses or 0) >= (invitation.maxUses or 1):
continue continue
# Get mandate name for notification # Get mandate name for notification using interface method
mandateId = invitation.get("mandateId") mandateId = invitation.mandateId
mandateRecords = appInterface.db.getRecordset( mandate = appInterface.getMandate(mandateId)
Mandate, mandateName = mandate.mandateLabel if mandate else "PowerOn"
recordFilter={"id": mandateId}
)
mandateName = mandateRecords[0].get("mandateLabel", "PowerOn") if mandateRecords else "PowerOn"
# Get inviter name # Get inviter name
inviterId = invitation.get("createdBy") inviterId = invitation.createdBy
inviter = appInterface.getUserById(inviterId) if inviterId else None inviter = appInterface.getUserById(inviterId) if inviterId else None
inviterName = (inviter.fullName or inviter.username) if inviter else "PowerOn" inviterName = (inviter.fullName or inviter.username) if inviter else "PowerOn"
createInvitationNotification( createInvitationNotification(
userId=str(user.id), userId=str(user.id),
invitationId=str(invitation.get("id")), invitationId=str(invitation.id),
mandateName=mandateName, mandateName=mandateName,
inviterName=inviterName 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: except Exception as notifErr:
logger.warning(f"Failed to create notifications for pending invitations: {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.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
from modules.datamodels.datamodelSecurity import Token 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 import createAccessToken, setAccessTokenCookie, createRefreshToken, setRefreshTokenCookie
from modules.auth.tokenManager import TokenManager from modules.auth.tokenManager import TokenManager
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
@ -97,11 +97,10 @@ async def login(
if connectionId: if connectionId:
try: try:
rootInterface = getRootInterface() rootInterface = getRootInterface()
# Fetch the connection by ID directly # Fetch the connection by ID directly using interface method
records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId}) connection = rootInterface.getUserConnectionById(connectionId)
if records: if connection:
record = records[0] login_hint = connection.externalEmail or connection.externalUsername
login_hint = record.get("externalEmail") or record.get("externalUsername")
if login_hint: if login_hint:
login_kwargs["login_hint"] = login_hint login_kwargs["login_hint"] = login_hint
# Derive domain hint from email/UPN # Derive domain hint from email/UPN

View file

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

View file

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

View file

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

View file

@ -39,7 +39,7 @@ class ExtractionService:
# Verify required internal model is available (used for pricing in extractContent) # Verify required internal model is available (used for pricing in extractContent)
modelDisplayName = "Internal Document Extractor" modelDisplayName = "Internal Document Extractor"
model = modelRegistry.getModel(modelDisplayName) 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.") raise RuntimeError(f"FATAL: Required internal model '{modelDisplayName}' is not available. Check connector registration.")
def extractContent( def extractContent(
@ -218,18 +218,18 @@ class ExtractionService:
modelDisplayName = "Internal Document Extractor" modelDisplayName = "Internal Document Extractor"
model = modelRegistry.getModel(modelDisplayName) model = modelRegistry.getModel(modelDisplayName)
# Hard fail if model is missing; caller must ensure connectors are registered # 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: if docOperationId:
self.services.chat.progressLogFinish(docOperationId, False) self.services.chat.progressLogFinish(docOperationId, False)
raise RuntimeError(f"Pricing model not available: {modelDisplayName}") 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 # Create AiCallResponse with real calculation
# Use model.name for the response (API identifier), not displayName # Use model.name for the response (API identifier), not displayName
aiResponse = AiCallResponse( aiResponse = AiCallResponse(
content="", # No content for extraction stats needed content="", # No content for extraction stats needed
modelName=model.name, modelName=model.name,
priceUsd=priceUsd, priceCHF=priceCHF,
processingTime=processingTime, processingTime=processingTime,
bytesSent=bytesSent, bytesSent=bytesSent,
bytesReceived=bytesReceived, bytesReceived=bytesReceived,
@ -478,7 +478,7 @@ class ExtractionService:
"resultSize": len(response.content), "resultSize": len(response.content),
"typeGroup": part.typeGroup, "typeGroup": part.typeGroup,
"modelName": response.modelName, "modelName": response.modelName,
"priceUsd": response.priceUsd "priceCHF": response.priceCHF
} }
) )
@ -606,7 +606,7 @@ class ExtractionService:
"originalIndex": i, # Phase 7: Explicit order index "originalIndex": i, # Phase 7: Explicit order index
"processingOrder": i, # Phase 7: Processing order "processingOrder": i, # Phase 7: Processing order
"modelName": result.modelName, "modelName": result.modelName,
"priceUsd": result.priceUsd, "priceCHF": result.priceCHF,
"processingTime": result.processingTime, "processingTime": result.processingTime,
"bytesSent": result.bytesSent, "bytesSent": result.bytesSent,
"bytesReceived": result.bytesReceived "bytesReceived": result.bytesReceived
@ -1311,7 +1311,7 @@ class ExtractionService:
return AiCallResponse( return AiCallResponse(
content=modelResponse.content, content=modelResponse.content,
modelName=model.name, modelName=model.name,
priceUsd=0.0, priceCHF=0.0,
processingTime=processingTime, processingTime=processingTime,
bytesSent=0, bytesSent=0,
bytesReceived=0, bytesReceived=0,
@ -1416,7 +1416,7 @@ class ExtractionService:
return AiCallResponse( return AiCallResponse(
content=mergedContent, content=mergedContent,
modelName=model.name, 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), processingTime=sum(r.processingTime for r in chunkResults),
bytesSent=sum(r.bytesSent for r in chunkResults), bytesSent=sum(r.bytesSent for r in chunkResults),
bytesReceived=sum(r.bytesReceived for r in chunkResults), bytesReceived=sum(r.bytesReceived for r in chunkResults),
@ -1465,7 +1465,7 @@ class ExtractionService:
return AiCallResponse( return AiCallResponse(
content=mergedContent, content=mergedContent,
modelName=model.name, 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), processingTime=sum(r.processingTime for r in chunkResults),
bytesSent=sum(r.bytesSent for r in chunkResults), bytesSent=sum(r.bytesSent for r in chunkResults),
bytesReceived=sum(r.bytesReceived for r in chunkResults), bytesReceived=sum(r.bytesReceived for r in chunkResults),
@ -1492,7 +1492,7 @@ class ExtractionService:
return AiCallResponse( return AiCallResponse(
content=errorMsg, content=errorMsg,
modelName="error", modelName="error",
priceUsd=0.0, priceCHF=0.0,
processingTime=0.0, processingTime=0.0,
bytesSent=inputBytes, bytesSent=inputBytes,
bytesReceived=outputBytes, bytesReceived=outputBytes,
@ -1622,7 +1622,7 @@ class ExtractionService:
return AiCallResponse( return AiCallResponse(
content=mergedContent, content=mergedContent,
modelName="multiple", 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), processingTime=sum(r.processingTime for r in allResults),
bytesSent=sum(r.bytesSent for r in allResults), bytesSent=sum(r.bytesSent for r in allResults),
bytesReceived=sum(r.bytesReceived 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() rootInterface = getRootInterface()
# Get all feature accesses for this user # Get all feature accesses for this user using interface method
featureAccesses = rootInterface.db.getRecordset( featureAccesses = rootInterface.getFeatureAccessesForUser(str(userId))
FeatureAccess,
recordFilter={"userId": str(userId)}
)
# Collect unique feature codes # Collect unique feature codes
featureCodes: Set[str] = set() featureCodes: Set[str] = set()
for fa in featureAccesses: for fa in featureAccesses:
instanceId = fa.get("featureInstanceId") instanceId = fa.featureInstanceId
instanceRecords = rootInterface.db.getRecordset( instance = rootInterface.getFeatureInstance(instanceId)
FeatureInstance, if instance:
recordFilter={"id": instanceId} featureCode = instance.featureCode
)
if instanceRecords:
featureCode = instanceRecords[0].get("featureCode")
if featureCode: if featureCode:
featureCodes.add(featureCode) featureCodes.add(featureCode)

View file

@ -25,11 +25,11 @@ FEATURE_ICON = "mdi-cog"
# Block Order (gemäss Navigation-API-Konzept): # Block Order (gemäss Navigation-API-Konzept):
# - System: 10 # - System: 10
# - <dynamic/features>: 15 (wird in routeSystem.py eingefügt) # - <dynamic/features>: 15 (wird in routeSystem.py eingefügt)
# - Workflows: 20
# - Basisdaten: 30 # - Basisdaten: 30
# - Migrate: 40
# - Administration: 200 # - Administration: 200
# #
# NOTE: Workflows and Migrate sections removed - now handled as features
#
# Item Order: Default-Abstand 10 pro Item # Item Order: Default-Abstand 10 pro Item
# uiComponent: Abgeleitet von objectKey (ui.system.home -> page.system.home) # uiComponent: Abgeleitet von objectKey (ui.system.home -> page.system.home)
# icon: Wird intern gehalten aber NICHT in der API Response zurückgegeben # 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", "id": "basedata",
"title": {"en": "BASE DATA", "de": "BASISDATEN", "fr": "DONNÉES DE BASE"}, "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", "id": "admin",
"title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"}, "title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"},
"order": 200, "order": 200,
"adminOnly": True, "adminOnly": True,
"items": [ "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", "id": "admin-mandates",
"objectKey": "ui.admin.mandates", "objectKey": "ui.admin.mandates",
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"}, "label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
"icon": "FaBuilding", "icon": "FaBuilding",
"path": "/admin/mandates", "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, "adminOnly": True,
}, },
{ {
@ -190,27 +148,36 @@ NAVIGATION_SECTIONS = [
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"}, "label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
"icon": "FaBuilding", "icon": "FaBuilding",
"path": "/admin/access", "path": "/admin/access",
"order": 5, "order": 30,
"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,
"adminOnly": True, "adminOnly": True,
}, },
{ {
"id": "admin-roles", "id": "admin-roles",
"objectKey": "ui.admin.roles", "objectKey": "ui.admin.roles",
"label": {"en": "Roles & Permissions", "de": "Rollen & Berechtigungen", "fr": "Rôles et permissions"}, "label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
"icon": "FaKey", "icon": "FaUserTag",
"path": "/admin/mandate-roles", "path": "/admin/mandate-roles",
"order": 40, "order": 40,
"adminOnly": True, "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( metadata=AiResponseMetadata(
additionalData={ additionalData={
"modelName": aiResponse_obj.modelName, "modelName": aiResponse_obj.modelName,
"priceUsd": aiResponse_obj.priceUsd, "priceCHF": aiResponse_obj.priceCHF,
"processingTime": aiResponse_obj.processingTime, "processingTime": aiResponse_obj.processingTime,
"bytesSent": aiResponse_obj.bytesSent, "bytesSent": aiResponse_obj.bytesSent,
"bytesReceived": aiResponse_obj.bytesReceived, "bytesReceived": aiResponse_obj.bytesReceived,

View file

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