fixed automation and trustee
This commit is contained in:
parent
a0b9a6e4b5
commit
50e3fce12b
8 changed files with 296 additions and 35 deletions
3
app.py
3
app.py
|
|
@ -47,6 +47,9 @@ class DailyRotatingFileHandler(RotatingFileHandler):
|
||||||
|
|
||||||
def _updateFileIfNeeded(self):
|
def _updateFileIfNeeded(self):
|
||||||
"""Update the log file if the date has changed"""
|
"""Update the log file if the date has changed"""
|
||||||
|
# Guard against interpreter shutdown when datetime may be None
|
||||||
|
if datetime is None:
|
||||||
|
return False
|
||||||
today = datetime.now().strftime("%Y%m%d")
|
today = datetime.now().strftime("%Y%m%d")
|
||||||
|
|
||||||
if self.currentDate != today:
|
if self.currentDate != today:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import json
|
||||||
|
|
||||||
# Import interfaces and models
|
# Import interfaces and models
|
||||||
from modules.interfaces.interfaceDbChat import getInterface as getChatInterface
|
from modules.interfaces.interfaceDbChat import getInterface as getChatInterface
|
||||||
from modules.auth import getCurrentUser, limiter
|
from modules.auth import getCurrentUser, limiter, getRequestContext, RequestContext
|
||||||
from modules.features.automation.datamodelFeatureAutomation import AutomationDefinition
|
from modules.features.automation.datamodelFeatureAutomation import AutomationDefinition
|
||||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||||
|
|
@ -46,7 +46,7 @@ router = APIRouter(
|
||||||
async def get_automations(
|
async def get_automations(
|
||||||
request: Request,
|
request: Request,
|
||||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||||
currentUser = Depends(getCurrentUser)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> PaginatedResponse[AutomationDefinition]:
|
) -> PaginatedResponse[AutomationDefinition]:
|
||||||
"""
|
"""
|
||||||
Get automation definitions with optional pagination, sorting, and filtering.
|
Get automation definitions with optional pagination, sorting, and filtering.
|
||||||
|
|
@ -69,7 +69,7 @@ async def get_automations(
|
||||||
detail=f"Invalid pagination parameter: {str(e)}"
|
detail=f"Invalid pagination parameter: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
chatInterface = getChatInterface(currentUser)
|
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||||
result = chatInterface.getAllAutomationDefinitions(pagination=paginationParams)
|
result = chatInterface.getAllAutomationDefinitions(pagination=paginationParams)
|
||||||
|
|
||||||
# If pagination was requested, result is PaginatedResult
|
# If pagination was requested, result is PaginatedResult
|
||||||
|
|
@ -111,11 +111,11 @@ async def get_automations(
|
||||||
async def create_automation(
|
async def create_automation(
|
||||||
request: Request,
|
request: Request,
|
||||||
automation: AutomationDefinition,
|
automation: AutomationDefinition,
|
||||||
currentUser = Depends(getCurrentUser)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> AutomationDefinition:
|
) -> AutomationDefinition:
|
||||||
"""Create a new automation definition"""
|
"""Create a new automation definition"""
|
||||||
try:
|
try:
|
||||||
chatInterface = getChatInterface(currentUser)
|
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||||
automationData = automation.model_dump()
|
automationData = automation.model_dump()
|
||||||
created = chatInterface.createAutomationDefinition(automationData)
|
created = chatInterface.createAutomationDefinition(automationData)
|
||||||
return created
|
return created
|
||||||
|
|
@ -132,7 +132,7 @@ async def create_automation(
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def get_automation_templates(
|
async def get_automation_templates(
|
||||||
request: Request,
|
request: Request,
|
||||||
currentUser = Depends(getCurrentUser)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get automation templates from backend module.
|
Get automation templates from backend module.
|
||||||
|
|
@ -160,11 +160,11 @@ async def get_automation_attributes(
|
||||||
async def get_automation(
|
async def get_automation(
|
||||||
request: Request,
|
request: Request,
|
||||||
automationId: str = Path(..., description="Automation ID"),
|
automationId: str = Path(..., description="Automation ID"),
|
||||||
currentUser = Depends(getCurrentUser)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> AutomationDefinition:
|
) -> AutomationDefinition:
|
||||||
"""Get a single automation definition by ID"""
|
"""Get a single automation definition by ID"""
|
||||||
try:
|
try:
|
||||||
chatInterface = getChatInterface(currentUser)
|
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||||
automation = chatInterface.getAutomationDefinition(automationId)
|
automation = chatInterface.getAutomationDefinition(automationId)
|
||||||
if not automation:
|
if not automation:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -188,11 +188,11 @@ async def update_automation(
|
||||||
request: Request,
|
request: Request,
|
||||||
automationId: str = Path(..., description="Automation ID"),
|
automationId: str = Path(..., description="Automation ID"),
|
||||||
automation: AutomationDefinition = Body(...),
|
automation: AutomationDefinition = Body(...),
|
||||||
currentUser = Depends(getCurrentUser)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> AutomationDefinition:
|
) -> AutomationDefinition:
|
||||||
"""Update an automation definition"""
|
"""Update an automation definition"""
|
||||||
try:
|
try:
|
||||||
chatInterface = getChatInterface(currentUser)
|
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||||
automationData = automation.model_dump()
|
automationData = automation.model_dump()
|
||||||
updated = chatInterface.updateAutomationDefinition(automationId, automationData)
|
updated = chatInterface.updateAutomationDefinition(automationId, automationData)
|
||||||
return updated
|
return updated
|
||||||
|
|
@ -215,11 +215,11 @@ async def update_automation(
|
||||||
async def delete_automation(
|
async def delete_automation(
|
||||||
request: Request,
|
request: Request,
|
||||||
automationId: str = Path(..., description="Automation ID"),
|
automationId: str = Path(..., description="Automation ID"),
|
||||||
currentUser = Depends(getCurrentUser)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""Delete an automation definition"""
|
"""Delete an automation definition"""
|
||||||
try:
|
try:
|
||||||
chatInterface = getChatInterface(currentUser)
|
chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
|
||||||
success = chatInterface.deleteAutomationDefinition(automationId)
|
success = chatInterface.deleteAutomationDefinition(automationId)
|
||||||
if success:
|
if success:
|
||||||
return Response(status_code=204)
|
return Response(status_code=204)
|
||||||
|
|
@ -244,15 +244,15 @@ async def delete_automation(
|
||||||
|
|
||||||
@router.post("/{automationId}/execute", response_model=ChatWorkflow)
|
@router.post("/{automationId}/execute", response_model=ChatWorkflow)
|
||||||
@limiter.limit("5/minute")
|
@limiter.limit("5/minute")
|
||||||
async def execute_automation(
|
async def execute_automation_route(
|
||||||
request: Request,
|
request: Request,
|
||||||
automationId: str = Path(..., description="Automation ID"),
|
automationId: str = Path(..., description="Automation ID"),
|
||||||
currentUser = Depends(getCurrentUser)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> ChatWorkflow:
|
) -> ChatWorkflow:
|
||||||
"""Execute an automation immediately (test mode)"""
|
"""Execute an automation immediately (test mode)"""
|
||||||
try:
|
try:
|
||||||
from modules.services import getInterface as getServices
|
from modules.services import getInterface as getServices
|
||||||
services = getServices(currentUser, None)
|
services = getServices(context.user, context.mandateId)
|
||||||
workflow = await executeAutomation(automationId, services)
|
workflow = await executeAutomation(automationId, services)
|
||||||
return workflow
|
return workflow
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ def discoverFeatureContainers() -> List[str]:
|
||||||
def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]:
|
def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Dynamically load and register routers from all discovered feature containers.
|
Dynamically load and register routers from all discovered feature containers.
|
||||||
|
Also registers feature template roles and AccessRules in the database.
|
||||||
"""
|
"""
|
||||||
results = {}
|
results = {}
|
||||||
pattern = os.path.join(FEATURES_DIR, "*", "routeFeature*.py")
|
pattern = os.path.join(FEATURES_DIR, "*", "routeFeature*.py")
|
||||||
|
|
@ -64,6 +65,15 @@ def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]:
|
||||||
logger.error(f"Failed to load router from {featureDir}: {e}")
|
logger.error(f"Failed to load router from {featureDir}: {e}")
|
||||||
results[featureDir] = {"status": "error", "error": str(e)}
|
results[featureDir] = {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
# Register features in RBAC catalog and sync template roles to database
|
||||||
|
from modules.security.rbacCatalog import getCatalogService
|
||||||
|
catalogService = getCatalogService()
|
||||||
|
registrationResults = registerAllFeaturesInCatalog(catalogService)
|
||||||
|
|
||||||
|
for featureName, success in registrationResults.items():
|
||||||
|
if featureName in results:
|
||||||
|
results[featureName]["rbac_registered"] = success
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ RESOURCE_OBJECTS = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Template roles for this feature
|
# Template roles for this feature with AccessRules
|
||||||
|
# Each role defines default UI and DATA permissions
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "trustee-admin",
|
"roleLabel": "trustee-admin",
|
||||||
|
|
@ -111,7 +112,13 @@ TEMPLATE_ROLES = [
|
||||||
"en": "Trustee Administrator - Full access to all trustee data and settings",
|
"en": "Trustee Administrator - Full access to all trustee data and settings",
|
||||||
"de": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
|
"de": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
|
||||||
"fr": "Administrateur fiduciaire - Accès complet aux données et paramètres fiduciaires"
|
"fr": "Administrateur fiduciaire - Accès complet aux données et paramètres fiduciaires"
|
||||||
}
|
},
|
||||||
|
"accessRules": [
|
||||||
|
# Full UI access
|
||||||
|
{"context": "UI", "item": None, "view": True},
|
||||||
|
# Full DATA access
|
||||||
|
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "trustee-accountant",
|
"roleLabel": "trustee-accountant",
|
||||||
|
|
@ -119,7 +126,13 @@ TEMPLATE_ROLES = [
|
||||||
"en": "Trustee Accountant - Manage accounting and financial data",
|
"en": "Trustee Accountant - Manage accounting and financial data",
|
||||||
"de": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
|
"de": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
|
||||||
"fr": "Comptable fiduciaire - Gérer les données comptables et financières"
|
"fr": "Comptable fiduciaire - Gérer les données comptables et financières"
|
||||||
}
|
},
|
||||||
|
"accessRules": [
|
||||||
|
# Full UI access
|
||||||
|
{"context": "UI", "item": None, "view": True},
|
||||||
|
# Group-level DATA access
|
||||||
|
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "trustee-client",
|
"roleLabel": "trustee-client",
|
||||||
|
|
@ -127,7 +140,13 @@ TEMPLATE_ROLES = [
|
||||||
"en": "Trustee Client - View own accounting data and documents",
|
"en": "Trustee Client - View own accounting data and documents",
|
||||||
"de": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
|
"de": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
|
||||||
"fr": "Client fiduciaire - Consulter ses propres données comptables et documents"
|
"fr": "Client fiduciaire - Consulter ses propres données comptables et documents"
|
||||||
}
|
},
|
||||||
|
"accessRules": [
|
||||||
|
# Full UI access
|
||||||
|
{"context": "UI", "item": None, "view": True},
|
||||||
|
# Own records only (MY level)
|
||||||
|
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -185,9 +204,198 @@ def registerFeature(catalogService) -> bool:
|
||||||
meta=resObj.get("meta")
|
meta=resObj.get("meta")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Sync template roles to database (with AccessRules)
|
||||||
|
_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()
|
||||||
|
db = rootInterface.db
|
||||||
|
|
||||||
|
# Get existing template roles for this feature
|
||||||
|
existingRoles = db.getRecordset(
|
||||||
|
Role,
|
||||||
|
recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
|
||||||
|
)
|
||||||
|
existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
|
||||||
|
|
||||||
|
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(db, 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 = db.recordCreate(Role, newRole.model_dump())
|
||||||
|
roleId = createdRole.get("id")
|
||||||
|
|
||||||
|
# Create AccessRules for this role
|
||||||
|
_ensureAccessRulesForRole(db, 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")
|
||||||
|
|
||||||
|
# Repair instance-specific roles that are missing AccessRules
|
||||||
|
_repairInstanceRolesAccessRules(db, existingRoleLabels)
|
||||||
|
|
||||||
|
return createdCount
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> int:
|
||||||
|
"""
|
||||||
|
Repair instance-specific roles by copying AccessRules from their template roles.
|
||||||
|
This ensures instance roles created before AccessRules were defined get updated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database connector
|
||||||
|
templateRoleLabels: Dict mapping roleLabel to template role ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of instance roles repaired
|
||||||
|
"""
|
||||||
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
|
||||||
|
repairedCount = 0
|
||||||
|
|
||||||
|
# Get all instance-specific roles for this feature (mandateId is NOT None)
|
||||||
|
allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
|
||||||
|
instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
|
||||||
|
|
||||||
|
for instanceRole in instanceRoles:
|
||||||
|
roleLabel = instanceRole.get("roleLabel")
|
||||||
|
instanceRoleId = instanceRole.get("id")
|
||||||
|
|
||||||
|
# Find matching template role
|
||||||
|
templateRoleId = templateRoleLabels.get(roleLabel)
|
||||||
|
if not templateRoleId:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if instance role has AccessRules
|
||||||
|
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
|
||||||
|
if existingRules:
|
||||||
|
continue # Already has rules, skip
|
||||||
|
|
||||||
|
# Copy AccessRules from template role
|
||||||
|
templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
|
||||||
|
if not templateRules:
|
||||||
|
continue # Template has no rules
|
||||||
|
|
||||||
|
for rule in templateRules:
|
||||||
|
newRule = AccessRule(
|
||||||
|
roleId=instanceRoleId,
|
||||||
|
context=rule.get("context"),
|
||||||
|
item=rule.get("item"),
|
||||||
|
view=rule.get("view", False),
|
||||||
|
read=rule.get("read"),
|
||||||
|
create=rule.get("create"),
|
||||||
|
update=rule.get("update"),
|
||||||
|
delete=rule.get("delete"),
|
||||||
|
)
|
||||||
|
db.recordCreate(AccessRule, newRule.model_dump())
|
||||||
|
|
||||||
|
logger.info(f"Repaired instance role '{roleLabel}' (ID: {instanceRoleId}): copied {len(templateRules)} AccessRules from template")
|
||||||
|
repairedCount += 1
|
||||||
|
|
||||||
|
if repairedCount > 0:
|
||||||
|
logger.info(f"Feature '{FEATURE_CODE}': Repaired {repairedCount} instance roles with missing AccessRules")
|
||||||
|
|
||||||
|
return repairedCount
|
||||||
|
|
||||||
|
|
||||||
|
def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
|
||||||
|
"""
|
||||||
|
Ensure AccessRules exist for a role based on templates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database connector
|
||||||
|
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
|
||||||
|
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
|
||||||
|
|
||||||
|
# Create a set of existing rule signatures to avoid duplicates
|
||||||
|
existingSignatures = set()
|
||||||
|
for rule in existingRules:
|
||||||
|
sig = (rule.get("context"), rule.get("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"),
|
||||||
|
)
|
||||||
|
db.recordCreate(AccessRule, newRule.model_dump())
|
||||||
|
createdCount += 1
|
||||||
|
|
||||||
|
if createdCount > 0:
|
||||||
|
logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
|
||||||
|
|
||||||
|
return createdCount
|
||||||
|
|
|
||||||
|
|
@ -1729,8 +1729,14 @@ class ChatObjects:
|
||||||
totalPages=totalPages
|
totalPages=totalPages
|
||||||
)
|
)
|
||||||
|
|
||||||
def getAutomationDefinition(self, automationId: str) -> Optional[AutomationDefinition]:
|
def getAutomationDefinition(self, automationId: str, includeSystemFields: bool = False) -> Optional[AutomationDefinition]:
|
||||||
"""Returns an automation definition by ID if user has access, with computed status."""
|
"""Returns an automation definition by ID if user has access, with computed status.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
automationId: ID of the automation to get
|
||||||
|
includeSystemFields: If True, returns raw dict with system fields (_createdBy, etc).
|
||||||
|
If False (default), returns Pydantic model without system fields.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# Use RBAC filtering
|
# Use RBAC filtering
|
||||||
filtered = getRecordsetWithRBAC(self.db,
|
filtered = getRecordsetWithRBAC(self.db,
|
||||||
|
|
@ -1749,6 +1755,16 @@ class ChatObjects:
|
||||||
automation["executionLogs"] = []
|
automation["executionLogs"] = []
|
||||||
# Enrich with user and mandate names
|
# Enrich with user and mandate names
|
||||||
self._enrichAutomationWithUserAndMandate(automation)
|
self._enrichAutomationWithUserAndMandate(automation)
|
||||||
|
|
||||||
|
# For internal use (execution), return raw dict with system fields
|
||||||
|
if includeSystemFields:
|
||||||
|
# Return as simple namespace object so getattr works
|
||||||
|
class AutomationWithSystemFields:
|
||||||
|
def __init__(self, data):
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
return AutomationWithSystemFields(automation)
|
||||||
|
|
||||||
# Clean metadata fields and return Pydantic model
|
# Clean metadata fields and return Pydantic model
|
||||||
cleanedRecord = {k: v for k, v in automation.items() if not k.startswith("_")}
|
cleanedRecord = {k: v for k, v in automation.items() if not k.startswith("_")}
|
||||||
return AutomationDefinition(**cleanedRecord)
|
return AutomationDefinition(**cleanedRecord)
|
||||||
|
|
@ -1771,9 +1787,12 @@ class ChatObjects:
|
||||||
|
|
||||||
# Ensure database connector has correct userId context
|
# Ensure database connector has correct userId context
|
||||||
# The connector should have been initialized with userId, but ensure it's updated
|
# The connector should have been initialized with userId, but ensure it's updated
|
||||||
if self.userId and hasattr(self.db, 'updateContext'):
|
if not self.userId:
|
||||||
|
logger.error(f"createAutomationDefinition: userId is not set! Cannot set _createdBy. currentUser={self.currentUser}")
|
||||||
|
elif hasattr(self.db, 'updateContext'):
|
||||||
try:
|
try:
|
||||||
self.db.updateContext(self.userId)
|
self.db.updateContext(self.userId)
|
||||||
|
logger.debug(f"createAutomationDefinition: Updated database context with userId={self.userId}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not update database context: {e}")
|
logger.warning(f"Could not update database context: {e}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,10 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
||||||
recordFilter={"userId": userId, "featureInstanceId": instanceId}
|
recordFilter={"userId": userId, "featureInstanceId": instanceId}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug(f"_getInstancePermissions: userId={userId}, instanceId={instanceId}, featureAccesses={len(featureAccesses) if featureAccesses else 0}")
|
||||||
|
|
||||||
if not featureAccesses:
|
if not featureAccesses:
|
||||||
|
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 FeatureAccessRole junction table
|
||||||
|
|
@ -259,7 +262,10 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
||||||
)
|
)
|
||||||
roleIds = [far.get("roleId") for far in featureAccessRoles]
|
roleIds = [far.get("roleId") for far in featureAccessRoles]
|
||||||
|
|
||||||
|
logger.debug(f"_getInstancePermissions: featureAccessId={featureAccessId}, roleIds={roleIds}")
|
||||||
|
|
||||||
if not roleIds:
|
if not roleIds:
|
||||||
|
logger.debug(f"_getInstancePermissions: No roles found for FeatureAccess {featureAccessId}")
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
# Get permissions (AccessRules) for all roles
|
# Get permissions (AccessRules) for all roles
|
||||||
|
|
@ -269,6 +275,8 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
||||||
recordFilter={"roleId": roleId}
|
recordFilter={"roleId": roleId}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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.get("context", "")
|
||||||
item = rule.get("item", "")
|
item = rule.get("item", "")
|
||||||
|
|
@ -303,8 +311,13 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
||||||
|
|
||||||
# Handle UI context (views)
|
# Handle UI context (views)
|
||||||
elif context == "UI" or context == AccessRuleContext.UI:
|
elif context == "UI" or context == AccessRuleContext.UI:
|
||||||
|
ruleView = rule.get("view", False)
|
||||||
if item:
|
if item:
|
||||||
permissions["views"][item] = permissions["views"].get(item, False) or rule.get("view", False)
|
# Specific view rule
|
||||||
|
permissions["views"][item] = permissions["views"].get(item, False) or ruleView
|
||||||
|
elif ruleView:
|
||||||
|
# item=None means all views - set a wildcard flag
|
||||||
|
permissions["views"]["_all"] = True
|
||||||
|
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ Shared utilities for model attributes and labels.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, ConfigDict
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from pydantic_core import PydanticUndefined
|
||||||
from typing import Dict, Any, List, Type, Optional, Union
|
from typing import Dict, Any, List, Type, Optional, Union
|
||||||
import inspect
|
import inspect
|
||||||
import importlib
|
import importlib
|
||||||
|
|
@ -212,7 +213,8 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
||||||
# Don't call it here - it's meant to be called per-instance
|
# Don't call it here - it's meant to be called per-instance
|
||||||
# Instead, store a marker that indicates it exists
|
# Instead, store a marker that indicates it exists
|
||||||
field_default = None # Frontend should use first option or specific logic
|
field_default = None # Frontend should use first option or specific logic
|
||||||
elif default_value is not ...: # Ellipsis means no default
|
elif default_value is not ... and default_value is not PydanticUndefined:
|
||||||
|
# Ellipsis or PydanticUndefined means no default
|
||||||
# Convert enum to its value if it's an enum
|
# Convert enum to its value if it's an enum
|
||||||
if hasattr(default_value, 'value'):
|
if hasattr(default_value, 'value'):
|
||||||
field_default = default_value.value
|
field_default = default_value.value
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,8 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Load automation definition
|
# 1. Load automation definition (with system fields for _createdBy access)
|
||||||
automation = services.interfaceDbChat.getAutomationDefinition(automationId)
|
automation = services.interfaceDbChat.getAutomationDefinition(automationId, includeSystemFields=True)
|
||||||
if not automation:
|
if not automation:
|
||||||
raise ValueError(f"Automation {automationId} not found")
|
raise ValueError(f"Automation {automationId} not found")
|
||||||
|
|
||||||
|
|
@ -105,18 +105,17 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
|
||||||
# 3. Get user who created automation
|
# 3. Get user who created automation
|
||||||
creatorUserId = getattr(automation, "_createdBy", None)
|
creatorUserId = getattr(automation, "_createdBy", None)
|
||||||
|
|
||||||
# CRITICAL: Automation MUST run as creator user only, or fail
|
# _createdBy is a system attribute - must be present
|
||||||
if not creatorUserId:
|
if not creatorUserId:
|
||||||
errorMsg = f"Automation {automationId} has no creator user (_createdBy field missing). Cannot execute automation."
|
errorMsg = f"Automation {automationId} has no creator user (_createdBy field missing). Cannot execute automation."
|
||||||
logger.error(errorMsg)
|
logger.error(errorMsg)
|
||||||
executionLog["messages"].append(errorMsg)
|
executionLog["messages"].append(errorMsg)
|
||||||
raise ValueError(errorMsg)
|
raise ValueError(errorMsg)
|
||||||
|
|
||||||
# Get user from database using services
|
# Get creator user from database
|
||||||
creatorUser = services.interfaceDbApp.getUser(creatorUserId)
|
creatorUser = services.interfaceDbApp.getUser(creatorUserId)
|
||||||
if not creatorUser:
|
if not creatorUser:
|
||||||
raise ValueError(f"Creator user {creatorUserId} not found")
|
raise ValueError(f"Creator user {creatorUserId} not found")
|
||||||
|
|
||||||
executionLog["messages"].append(f"Using creator user: {creatorUserId}")
|
executionLog["messages"].append(f"Using creator user: {creatorUserId}")
|
||||||
|
|
||||||
# 4. Create UserInputRequest from plan
|
# 4. Create UserInputRequest from plan
|
||||||
|
|
@ -205,10 +204,17 @@ async def syncAutomationEvents(services, eventUser) -> Dict[str, Any]:
|
||||||
registeredEvents = {}
|
registeredEvents = {}
|
||||||
|
|
||||||
for automation in filtered:
|
for automation in filtered:
|
||||||
automationId = automation.id
|
# Handle both dict and object access patterns
|
||||||
isActive = automation.active if hasattr(automation, 'active') else False
|
if isinstance(automation, dict):
|
||||||
currentEventId = automation.eventId if hasattr(automation, 'eventId') else None
|
automationId = automation.get('id')
|
||||||
schedule = automation.schedule if hasattr(automation, 'schedule') else None
|
isActive = automation.get('active', False)
|
||||||
|
currentEventId = automation.get('eventId')
|
||||||
|
schedule = automation.get('schedule')
|
||||||
|
else:
|
||||||
|
automationId = automation.id
|
||||||
|
isActive = automation.active if hasattr(automation, 'active') else False
|
||||||
|
currentEventId = automation.eventId if hasattr(automation, 'eventId') else None
|
||||||
|
schedule = automation.schedule if hasattr(automation, 'schedule') else None
|
||||||
|
|
||||||
if not schedule:
|
if not schedule:
|
||||||
logger.warning(f"Automation {automationId} has no schedule, skipping")
|
logger.warning(f"Automation {automationId} has no schedule, skipping")
|
||||||
|
|
@ -287,8 +293,8 @@ def createAutomationEventHandler(automationId: str, eventUser):
|
||||||
# Get services for event user (provides access to interfaces)
|
# Get services for event user (provides access to interfaces)
|
||||||
eventServices = getServices(eventUser, None)
|
eventServices = getServices(eventUser, None)
|
||||||
|
|
||||||
# Load automation using event user context
|
# Load automation using event user context (with system fields for _createdBy access)
|
||||||
automation = eventServices.interfaceDbChat.getAutomationDefinition(automationId)
|
automation = eventServices.interfaceDbChat.getAutomationDefinition(automationId, includeSystemFields=True)
|
||||||
if not automation or not getattr(automation, "active", False):
|
if not automation or not getattr(automation, "active", False):
|
||||||
logger.warning(f"Automation {automationId} not found or not active, skipping execution")
|
logger.warning(f"Automation {automationId} not found or not active, skipping execution")
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue