gateway/modules/interfaces/interfaceBootstrap.py
2026-01-13 20:01:50 +01:00

1220 lines
40 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Centralized bootstrap interface for system initialization.
Contains all bootstrap logic including mandate, users, and RBAC rules.
"""
import logging
from typing import Optional, List, Dict, Any
from passlib.context import CryptContext
from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.shared.configuration import APP_CONFIG
from modules.datamodels.datamodelUam import (
Mandate,
UserInDB,
AuthAuthority,
)
from modules.datamodels.datamodelRbac import (
AccessRule,
AccessRuleContext,
Role,
)
from modules.datamodels.datamodelUam import AccessLevel
logger = logging.getLogger(__name__)
# Password-Hashing
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
def initBootstrap(db: DatabaseConnector) -> None:
"""
Main bootstrap entry point - initializes all system components.
Args:
db: Database connector instance
"""
logger.info("Starting system bootstrap")
# Initialize root mandate
mandateId = initRootMandate(db)
# Initialize admin user
adminUserId = initAdminUser(db, mandateId)
# Initialize event user
eventUserId = initEventUser(db, mandateId)
# Initialize roles
initRoles(db)
# Initialize RBAC rules
initRbacRules(db)
# Assign initial user roles
if adminUserId and eventUserId:
assignInitialUserRoles(db, adminUserId, eventUserId)
logger.info("System bootstrap completed")
def initRootMandate(db: DatabaseConnector) -> Optional[str]:
"""
Creates the Root mandate if it doesn't exist.
Args:
db: Database connector instance
Returns:
Mandate ID if created or found, None otherwise
"""
existingMandates = db.getRecordset(Mandate)
if existingMandates:
mandateId = existingMandates[0].get("id")
logger.info(f"Root mandate already exists with ID {mandateId}")
return mandateId
logger.info("Creating Root mandate")
rootMandate = Mandate(name="Root", language="en", enabled=True)
createdMandate = db.recordCreate(Mandate, rootMandate)
mandateId = createdMandate.get("id")
logger.info(f"Root mandate created with ID {mandateId}")
return mandateId
def initAdminUser(db: DatabaseConnector, mandateId: Optional[str]) -> Optional[str]:
"""
Creates the Admin user if it doesn't exist.
Args:
db: Database connector instance
mandateId: Root mandate ID
Returns:
User ID if created or found, None otherwise
"""
existingUsers = db.getRecordset(UserInDB, recordFilter={"username": "admin"})
if existingUsers:
userId = existingUsers[0].get("id")
logger.info(f"Admin user already exists with ID {userId}")
return userId
logger.info("Creating Admin user")
adminUser = UserInDB(
mandateId=mandateId,
username="admin",
email="admin@example.com",
fullName="Administrator",
enabled=True,
language="en",
roleLabels=["sysadmin"],
authenticationAuthority=AuthAuthority.LOCAL,
hashedPassword=_getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_ADMIN_SECRET")),
connections=[],
)
createdUser = db.recordCreate(UserInDB, adminUser)
userId = createdUser.get("id")
logger.info(f"Admin user created with ID {userId}")
return userId
def initEventUser(db: DatabaseConnector, mandateId: Optional[str]) -> Optional[str]:
"""
Creates the Event user if it doesn't exist.
Args:
db: Database connector instance
mandateId: Root mandate ID
Returns:
User ID if created or found, None otherwise
"""
existingUsers = db.getRecordset(UserInDB, recordFilter={"username": "event"})
if existingUsers:
userId = existingUsers[0].get("id")
logger.info(f"Event user already exists with ID {userId}")
return userId
logger.info("Creating Event user")
eventUser = UserInDB(
mandateId=mandateId,
username="event",
email="event@example.com",
fullName="Event",
enabled=True,
language="en",
roleLabels=["sysadmin"],
authenticationAuthority=AuthAuthority.LOCAL,
hashedPassword=_getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_EVENT_SECRET")),
connections=[],
)
createdUser = db.recordCreate(UserInDB, eventUser)
userId = createdUser.get("id")
logger.info(f"Event user created with ID {userId}")
return userId
def initRoles(db: DatabaseConnector) -> None:
"""
Initialize standard roles if they don't exist.
Args:
db: Database connector instance
"""
logger.info("Initializing roles")
standardRoles = [
Role(
roleLabel="sysadmin",
description={"en": "System Administrator - Full access to all system resources", "fr": "Administrateur système - Accès complet à toutes les ressources"},
isSystemRole=True
),
Role(
roleLabel="admin",
description={"en": "Administrator - Manage users and resources within mandate scope", "fr": "Administrateur - Gérer les utilisateurs et ressources dans le périmètre du mandat"},
isSystemRole=True
),
Role(
roleLabel="user",
description={"en": "User - Standard user with access to own records", "fr": "Utilisateur - Utilisateur standard avec accès à ses propres enregistrements"},
isSystemRole=True
),
Role(
roleLabel="viewer",
description={"en": "Viewer - Read-only access to group records", "fr": "Visualiseur - Accès en lecture seule aux enregistrements du groupe"},
isSystemRole=True
),
]
existingRoles = db.getRecordset(Role)
existingRoleLabels = {role.get("roleLabel") for role in existingRoles}
for role in standardRoles:
if role.roleLabel not in existingRoleLabels:
try:
db.recordCreate(Role, role)
logger.info(f"Created role: {role.roleLabel}")
except Exception as e:
logger.warning(f"Error creating role {role.roleLabel}: {e}")
else:
logger.debug(f"Role {role.roleLabel} already exists")
logger.info("Roles initialization completed")
def initRbacRules(db: DatabaseConnector) -> None:
"""
Initialize RBAC rules if they don't exist.
Converts all UAM logic from interface*Access.py modules to RBAC rules.
Also checks for and adds missing rules for new tables.
Args:
db: Database connector instance
"""
existingRules = db.getRecordset(AccessRule)
if existingRules:
logger.info(f"RBAC rules already exist ({len(existingRules)} rules)")
# Check for missing rules for ChatWorkflow and Prompt tables
_addMissingTableRules(db, existingRules)
return
logger.info("Initializing RBAC rules")
# Create default role rules
createDefaultRoleRules(db)
# Create table-specific rules (converted from UAM logic)
createTableSpecificRules(db)
# Create UI context rules
createUiContextRules(db)
# Create RESOURCE context rules
createResourceContextRules(db)
# Create Action-specific RBAC rules
createActionRules(db)
logger.info("RBAC rules initialization completed")
def createDefaultRoleRules(db: DatabaseConnector) -> None:
"""
Create default role rules for generic access (item = null).
Args:
db: Database connector instance
"""
defaultRules = [
# SysAdmin Role - Full access to all
AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item=None,
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
),
# Admin Role - Group-level access
AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item=None,
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.NONE,
),
# User Role - My records only
AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item=None,
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
),
# Viewer Role - Read-only group access
AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item=None,
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
),
]
for rule in defaultRules:
db.recordCreate(AccessRule, rule)
logger.info(f"Created {len(defaultRules)} default role rules")
def createTableSpecificRules(db: DatabaseConnector) -> None:
"""
Create table-specific rules converted from UAM logic.
These rules override generic rules for specific tables.
Args:
db: Database connector instance
"""
tableRules = []
# Mandate table - Only sysadmin can access
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item="Mandate",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="Mandate",
view=False,
read=AccessLevel.NONE,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="Mandate",
view=False,
read=AccessLevel.NONE,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item="Mandate",
view=False,
read=AccessLevel.NONE,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# UserInDB table
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item="UserInDB",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="UserInDB",
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="UserInDB",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.MY,
delete=AccessLevel.NONE,
))
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item="UserInDB",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# UserConnection table
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item="UserConnection",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="UserConnection",
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="UserConnection",
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item="UserConnection",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# DataNeutraliserConfig table
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item="DataNeutraliserConfig",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="DataNeutraliserConfig",
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="DataNeutraliserConfig",
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item="DataNeutraliserConfig",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# DataNeutralizerAttributes table
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item="DataNeutralizerAttributes",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="DataNeutralizerAttributes",
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="DataNeutralizerAttributes",
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item="DataNeutralizerAttributes",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# AuthEvent table
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item="AuthEvent",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="AuthEvent",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="AuthEvent",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item="AuthEvent",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# ChatWorkflow table - Users can access their own workflows
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item="ChatWorkflow",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="ChatWorkflow",
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="ChatWorkflow",
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item="ChatWorkflow",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# Prompt table - Users can access their own prompts
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item="Prompt",
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item="Prompt",
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item="Prompt",
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item="Prompt",
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# Real Estate tables - Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land
realEstateTables = ["Projekt", "Parzelle", "Dokument", "Gemeinde", "Kanton", "Land"]
for table in realEstateTables:
# Sysadmin - full access
tableRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
# Admin - group access
tableRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
# User - my records only
tableRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
# Viewer - read-only my records
tableRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# Create all table-specific rules
for rule in tableRules:
db.recordCreate(AccessRule, rule)
logger.info(f"Created {len(tableRules)} table-specific rules")
def createUiContextRules(db: DatabaseConnector) -> None:
"""
Create UI context rules for controlling UI element visibility.
These rules control which UI components users can see based on their roles.
Args:
db: Database connector instance
"""
uiRules = []
# Generic UI rules - all roles can view UI by default
# Specific UI elements can override these with more restrictive rules
# Sysadmin - full UI access
uiRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.UI,
item=None,
view=True,
read=None,
create=None,
update=None,
delete=None,
))
# Admin - full UI access
uiRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.UI,
item=None,
view=True,
read=None,
create=None,
update=None,
delete=None,
))
# User - full UI access
uiRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.UI,
item=None,
view=True,
read=None,
create=None,
update=None,
delete=None,
))
# Viewer - full UI access (can view but may have restricted actions)
uiRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.UI,
item=None,
view=True,
read=None,
create=None,
update=None,
delete=None,
))
# Create all UI context rules
for rule in uiRules:
db.recordCreate(AccessRule, rule)
logger.info(f"Created {len(uiRules)} UI context rules")
def createResourceContextRules(db: DatabaseConnector) -> None:
"""
Create RESOURCE context rules for controlling resource access (AI models, actions, etc.).
These rules control which resources users can access based on their roles.
Args:
db: Database connector instance
"""
resourceRules = []
# Generic resource rules - all roles can access resources by default
# Specific resources can override these with more restrictive rules
# Sysadmin - full resource access
resourceRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.RESOURCE,
item=None,
view=True,
read=None,
create=None,
update=None,
delete=None,
))
# Admin - full resource access
resourceRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.RESOURCE,
item=None,
view=True,
read=None,
create=None,
update=None,
delete=None,
))
# User - full resource access
resourceRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.RESOURCE,
item=None,
view=True,
read=None,
create=None,
update=None,
delete=None,
))
# Viewer - full resource access (can view but may have restricted actions)
resourceRules.append(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.RESOURCE,
item=None,
view=True,
read=None,
create=None,
update=None,
delete=None,
))
# Create all RESOURCE context rules
for rule in resourceRules:
db.recordCreate(AccessRule, rule)
logger.info(f"Created {len(resourceRules)} RESOURCE context rules")
def createActionRules(db: DatabaseConnector) -> None:
"""
Create default RBAC rules for workflow actions.
This function dynamically discovers all available actions from all methods
and creates RBAC rules for them. Actions are protected via RESOURCE context
with actionId as the item identifier (format: 'module.actionName').
Args:
db: Database connector instance
"""
try:
# Import method discovery to get all actions
from modules.workflows.processing.shared.methodDiscovery import discoverMethods
from modules.services import getInterface as getServices
from modules.datamodels.datamodelUam import User
# Create a temporary user context for discovery (will be filtered by RBAC later)
# We need to discover methods, but we'll use a minimal user context
# In production, this should use a system user or admin user
try:
# Try to get an admin user for discovery
adminUsers = db.getRecordset("User", recordFilter={"roleLabel": "sysadmin"}, limit=1)
if adminUsers:
tempUser = User(**adminUsers[0])
else:
# Fallback: create minimal user context
tempUser = User(id="system", roleLabel="sysadmin")
except:
# Fallback: create minimal user context
tempUser = User(id="system", roleLabel="sysadmin")
# Get services and discover methods
services = getServices(tempUser, None)
discoverMethods(services)
# Import methods catalog
from modules.workflows.processing.shared.methodDiscovery import methods
# Collect all action IDs
allActionIds = []
for methodName, methodInfo in methods.items():
# Skip duplicate entries (same method stored with full and short name)
if methodName.startswith('Method'):
continue
methodInstance = methodInfo['instance']
methodActions = methodInstance.actions
for actionName in methodActions.keys():
actionId = f"{methodInstance.name}.{actionName}"
allActionIds.append(actionId)
logger.info(f"Discovered {len(allActionIds)} actions for RBAC rule creation")
# Define default action access by role
# SysAdmin and Admin: Access to all actions
# User: Access to common actions (read, search, process, etc.)
# Viewer: Read-only actions
actionRules = []
# All roles: Generic access to all actions
# Using item=None grants access to all resources (all actions) in RESOURCE context
# SysAdmin: Access to all actions
actionRules.append(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.RESOURCE,
item=None, # All resources (covers all actions)
view=True
))
# Admin: Access to all actions
actionRules.append(AccessRule(
roleLabel="admin",
context=AccessRuleContext.RESOURCE,
item=None, # All resources (covers all actions)
view=True
))
# User: Access to all actions (generic rights)
actionRules.append(AccessRule(
roleLabel="user",
context=AccessRuleContext.RESOURCE,
item=None, # All resources (covers all actions)
view=True
))
# Create all action rules
for rule in actionRules:
db.recordCreate(AccessRule, rule)
logger.info(f"Created {len(actionRules)} action RBAC rules")
except Exception as e:
logger.error(f"Error creating action RBAC rules: {str(e)}", exc_info=True)
# Don't fail bootstrap if action rules can't be created
# They can be created manually or via migration script
def _addMissingTableRules(db: DatabaseConnector, existingRules: List[Dict[str, Any]]) -> None:
"""
Add missing RBAC rules for tables that were added after initial bootstrap.
Args:
db: Database connector instance
existingRules: List of existing AccessRule records
"""
# Check which tables already have rules
existingItems = {rule.get("item") for rule in existingRules if rule.get("context") == AccessRuleContext.DATA}
existingRoles = {rule.get("roleLabel") for rule in existingRules}
# Tables that need rules
requiredTables = [
"ChatWorkflow", "Prompt", "Projekt", "Parzelle", "Dokument", "Gemeinde", "Kanton", "Land",
# Trustee tables
"TrusteeOrganisation", "TrusteeRole", "TrusteeAccess", "TrusteeContract",
"TrusteeDocument", "TrusteePosition", "TrusteePositionDocument"
]
requiredRoles = ["sysadmin", "admin", "user", "viewer"]
newRules = []
for table in requiredTables:
if table not in existingItems:
logger.info(f"Adding missing RBAC rules for table {table}")
# ChatWorkflow rules
if table == "ChatWorkflow":
for roleLabel in requiredRoles:
if roleLabel == "sysadmin":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
elif roleLabel == "admin":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
elif roleLabel == "user":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
elif roleLabel == "viewer":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# Prompt rules (same as ChatWorkflow)
elif table == "Prompt":
for roleLabel in requiredRoles:
if roleLabel == "sysadmin":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
elif roleLabel == "admin":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
elif roleLabel == "user":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
elif roleLabel == "viewer":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# Real Estate tables rules (Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land)
elif table in ["Projekt", "Parzelle", "Dokument", "Gemeinde", "Kanton", "Land"]:
for roleLabel in requiredRoles:
if roleLabel == "sysadmin":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
elif roleLabel == "admin":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
elif roleLabel == "user":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
elif roleLabel == "viewer":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# Trustee tables rules
elif table in ["TrusteeOrganisation", "TrusteeRole", "TrusteeAccess", "TrusteeContract",
"TrusteeDocument", "TrusteePosition", "TrusteePositionDocument"]:
for roleLabel in requiredRoles:
if roleLabel == "sysadmin":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
elif roleLabel == "admin":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.GROUP,
create=AccessLevel.GROUP,
update=AccessLevel.GROUP,
delete=AccessLevel.GROUP,
))
elif roleLabel == "user":
# User role: Access to MY records (feature-specific access via trustee.access)
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
elif roleLabel == "viewer":
newRules.append(AccessRule(
roleLabel=roleLabel,
context=AccessRuleContext.DATA,
item=table,
view=True,
read=AccessLevel.MY,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# Create missing rules
if newRules:
for rule in newRules:
db.recordCreate(AccessRule, rule)
logger.info(f"Added {len(newRules)} missing RBAC rules")
def assignInitialUserRoles(db: DatabaseConnector, adminUserId: str, eventUserId: str) -> None:
"""
Assign initial roles to admin and event users.
Args:
db: Database connector instance
adminUserId: Admin user ID
eventUserId: Event user ID
"""
# Set context to admin user for bootstrap operations
originalUserId = db.userId if hasattr(db, 'userId') else None
try:
if adminUserId:
db.updateContext(adminUserId)
# Update admin user with sysadmin role
adminUser = db.getRecordset(UserInDB, recordFilter={"id": adminUserId})
if adminUser:
adminUserData = adminUser[0]
roleLabels = adminUserData.get("roleLabels") or []
if "sysadmin" not in roleLabels:
adminUserData["roleLabels"] = roleLabels + ["sysadmin"]
db.recordModify(UserInDB, adminUserId, adminUserData)
logger.info(f"Assigned sysadmin role to admin user {adminUserId}")
# Update event user with sysadmin role
eventUser = db.getRecordset(UserInDB, recordFilter={"id": eventUserId})
if eventUser:
eventUserData = eventUser[0]
roleLabels = eventUserData.get("roleLabels") or []
if "sysadmin" not in roleLabels:
eventUserData["roleLabels"] = roleLabels + ["sysadmin"]
db.recordModify(UserInDB, eventUserId, eventUserData)
logger.info(f"Assigned sysadmin role to event user {eventUserId}")
finally:
# Restore original context if it existed
if originalUserId:
db.updateContext(originalUserId)
elif hasattr(db, 'userId'):
# If original was None/empty, just set it directly
db.userId = originalUserId
def _getPasswordHash(password: Optional[str]) -> Optional[str]:
"""
Hash a password using Argon2.
Args:
password: Plain text password
Returns:
Hashed password or None if password is None
"""
if password is None:
return None
return pwdContext.hash(password)