fix: cleaned interfaces to work all with rbac

This commit is contained in:
ValueOn AG 2026-01-11 15:42:33 +01:00
parent abbed64463
commit 5380e30f0d
12 changed files with 415 additions and 1616 deletions

View file

@ -29,6 +29,13 @@ DB_MANAGEMENT_USER=heeshkdlby
DB_MANAGEMENT_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjTnJKNlJMNmEwQ0Y5dVNrR3pkZk9SQXVvLTRTNW9lQ1g3TTE5cFhBNTd5UENqWW9qdWd3NWNseWhnUHJveDJyd1Z3X1czS3VuZnAwZHBXYVNQWlZsRy12ME42NndEVlR5X3ZPdFBNNmhLYm89
DB_MANAGEMENT_PORT=5432
# PostgreSQL Storage (new)
DB_REALESTATE_HOST=localhost
DB_REALESTATE_DATABASE=poweron_realestate
DB_REALESTATE_USER=poweron_dev
DB_REALESTATE_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjTnJKNlJMNmEwQ0Y5dVNrR3pkZk9SQXVvLTRTNW9lQ1g3TTE5cFhBNTd5UENqWW9qdWd3NWNseWhnUHJveDJyd1Z3X1czS3VuZnAwZHBXYVNQWlZsRy12ME42NndEVlR5X3ZPdFBNNmhLYm89
DB_REALESTATE_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNUctb2RwU25iR3ZnanBOdHZhWUtIajZ1RnZzTEp4aDR0MktWRjNoeVBrY1Npd1R0VE9YVHp3M2w1cXRzbUxNaU82QUJvaDNFeVQyN05KblRWblBvbWtoT0VXbkNBbDQ5OHhwSUFnaDZGRG10Vmgtdm1YUkRsYUhFMzRVZURmSFlDTFIzVWg4MXNueDZyMGc5aVpFdWRxY3dkTExGM093ZTVUZVl5LUhGWnlRPQ==
APP_TOKEN_EXPIRY=300

View file

@ -29,6 +29,13 @@ DB_MANAGEMENT_USER=gzxxmcrdhn
DB_MANAGEMENT_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3ZWpySThqdlVmWWd5dGxmWE91RVBsenZrQmNhSzVxbktmYzZ1RlM3cXhTMUdXRV9wX1lfLTJXLTFzeUo0R3pWLXlmUWdrZ2x6QkFlZVRXaEF6aUdRbDlzb1FfcWtub0dxSGp3OVVQWGg3enM9
DB_MANAGEMENT_PORT=5432
# PostgreSQL Storage (new)
DB_REALESTATE_HOST=localhost
DB_REALESTATE_DATABASE=poweron_realestate
DB_REALESTATE_USER=poweron_dev
DB_REALESTATE_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3ZWpySThqdlVmWWd5dGxmWE91RVBsenZrQmNhSzVxbktmYzZ1RlM3cXhTMUdXRV9wX1lfLTJXLTFzeUo0R3pWLXlmUWdrZ2x6QkFlZVRXaEF6aUdRbDlzb1FfcWtub0dxSGp3OVVQWGg3enM9
DB_REALESTATE_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3elhfV0Rnd2pQRjlMdkVwX1FnSmRhSzNZUlV5SVpaWXBNX1hpa2xPZGdMSWpnN2ZINHQxeGZnNHJweU5pZjlyYlY5Qm9zOUZEbl9wUEgtZHZXd1NhR19JSG9kbFU4MnFGQnllbFhRQVphRGQyNHlFVWR5VHQyUUpqN0stUmRuY2QyTi1oalczRHpLTEJqWURjZWs4YjZvT2U5YnFqcXEwdEpxV05fX05QMmtrPQ==
APP_TOKEN_EXPIRY=300

View file

@ -51,6 +51,7 @@ def _get_model_fields(model_class) -> Dict[str, str]:
field_type = field_info.annotation
# Check for JSONB fields (Dict, List, or complex types)
# Purely type-based detection - no hardcoded field names
if (
field_type == dict
or field_type == list
@ -58,23 +59,7 @@ def _get_model_fields(model_class) -> Dict[str, str]:
hasattr(field_type, "__origin__")
and field_type.__origin__ in (dict, list)
)
or field_name
in [
"execParameters",
"expectedDocumentFormats",
"resultDocuments",
"logs",
"messages",
"stats",
"tasks",
"perimeter", # GeoPolylinie objects
"baulinie", # GeoPolylinie objects
"kontextInformationen", # List of Kontext objects
"parzellenNachbarschaft", # List of dictionaries
"dokumente", # List of Dokument objects
"parzellen", # List of Parzelle objects (in Projekt)
]
# Check if field type is a Pydantic BaseModel (for nested models like GeoPolylinie)
# Check if field type is a Pydantic BaseModel (for nested models)
or (hasattr(field_type, "__origin__") and get_origin(field_type) is Union
and any(hasattr(arg, "__bases__") and BaseModel in getattr(arg, "__bases__", ())
for arg in get_args(field_type)))
@ -691,21 +676,28 @@ class DatabaseConnector:
# Handle JSONB fields for all records
fields = _get_model_fields(model_class)
model_fields = model_class.model_fields # Get Pydantic model fields
for record in records:
for field_name, field_type in fields.items():
if field_type == "JSONB" and field_name in record:
if record[field_name] is None:
# Convert None to appropriate default based on field name
if field_name in [
"logs",
"messages",
"tasks",
"expectedDocumentFormats",
"resultDocuments",
]:
record[field_name] = []
elif field_name in ["execParameters", "stats"]:
record[field_name] = {}
# Generic type-based default: List types -> [], Dict types -> {}
# Interfaces handle domain-specific defaults
field_info = model_fields.get(field_name)
if field_info:
field_annotation = field_info.annotation
# Check if it's a List type
if (field_annotation == list or
(hasattr(field_annotation, "__origin__") and
field_annotation.__origin__ is list)):
record[field_name] = []
# Check if it's a Dict type
elif (field_annotation == dict or
(hasattr(field_annotation, "__origin__") and
field_annotation.__origin__ is dict)):
record[field_name] = {}
else:
record[field_name] = None
else:
record[field_name] = None
else:
@ -878,6 +870,7 @@ class DatabaseConnector:
# Handle JSONB fields and ensure numeric types are correct
fields = _get_model_fields(model_class)
model_fields = model_class.model_fields # Get Pydantic model fields
for record in records:
for field_name, field_type in fields.items():
# Ensure numeric fields (float/int) are properly typed
@ -897,17 +890,23 @@ class DatabaseConnector:
)
elif field_type == "JSONB" and field_name in record:
if record[field_name] is None:
# Convert None to appropriate default based on field name
if field_name in [
"logs",
"messages",
"tasks",
"expectedDocumentFormats",
"resultDocuments",
]:
record[field_name] = []
elif field_name in ["execParameters", "stats"]:
record[field_name] = {}
# Generic type-based default: List types -> [], Dict types -> {}
# Interfaces handle domain-specific defaults
field_info = model_fields.get(field_name)
if field_info:
field_annotation = field_info.annotation
# Check if it's a List type
if (field_annotation == list or
(hasattr(field_annotation, "__origin__") and
field_annotation.__origin__ is list)):
record[field_name] = []
# Check if it's a Dict type
elif (field_annotation == dict or
(hasattr(field_annotation, "__origin__") and
field_annotation.__origin__ is dict)):
record[field_name] = {}
else:
record[field_name] = None
else:
record[field_name] = None
else:

View file

@ -878,7 +878,7 @@ class SwissTopoMapServerConnector:
return None
x, y = coords
logger.info(f"Parzelle gefunden: {parcel.get('label', 'Unknown')}, Zentrum: E={x}, N={y}")
logger.info(f"Parzelle gefunden: {search_result.get('label', 'Unknown')}, Zentrum: E={x}, N={y}")
# Schritt 2: Polygon-Geometrie abrufen
identify_params = {

View file

@ -645,6 +645,54 @@ def createTableSpecificRules(db: DatabaseConnector) -> 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)
@ -903,7 +951,7 @@ def _addMissingTableRules(db: DatabaseConnector, existingRules: List[Dict[str, A
existingRoles = {rule.get("roleLabel") for rule in existingRules}
# Tables that need rules
requiredTables = ["ChatWorkflow", "Prompt"]
requiredTables = ["ChatWorkflow", "Prompt", "Projekt", "Parzelle", "Dokument", "Gemeinde", "Kanton", "Land"]
requiredRoles = ["sysadmin", "admin", "user", "viewer"]
newRules = []
@ -1005,6 +1053,53 @@ def _addMissingTableRules(db: DatabaseConnector, existingRules: List[Dict[str, A
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,
))
# Create missing rules
if newRules:

View file

@ -134,6 +134,47 @@ class AppObjects:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def _separateObjectFields(self, model_class, data: Dict[str, Any]) -> tuple[Dict[str, Any], Dict[str, Any]]:
"""Separate simple fields from object fields based on Pydantic model structure."""
simpleFields = {}
objectFields = {}
# Get field information from the Pydantic model
modelFields = model_class.model_fields
for fieldName, value in data.items():
# Check if this field should be stored as JSONB in the database
if fieldName in modelFields:
fieldInfo = modelFields[fieldName]
# Pydantic v2 only
fieldType = fieldInfo.annotation
# Check if this is a JSONB field (Dict, List, or complex types)
# Purely type-based detection - no hardcoded field names
if (fieldType == dict or
fieldType == list or
(hasattr(fieldType, '__origin__') and fieldType.__origin__ in (dict, list))):
# Store as JSONB - include in simple_fields for database storage
simpleFields[fieldName] = value
elif isinstance(value, (str, int, float, bool, type(None))):
# Simple scalar types
simpleFields[fieldName] = value
else:
# Complex objects that should be filtered out
objectFields[fieldName] = value
else:
# Field not in model - treat as scalar if simple, otherwise filter out
# BUT: always include metadata fields (_createdBy, _createdAt, etc.) as they're handled by connector
if fieldName.startswith("_"):
# Metadata fields should be passed through to connector
simpleFields[fieldName] = value
elif isinstance(value, (str, int, float, bool, type(None))):
simpleFields[fieldName] = value
else:
objectFields[fieldName] = value
return simpleFields, objectFields
def _initRecords(self):
"""Initialize standard records if they don't exist."""
initBootstrap(self.db)

View file

@ -223,15 +223,16 @@ class ChatObjects:
fieldType = fieldInfo.annotation
# Always route relational/object fields to object_fields for separate handling
if fieldName in ['documents', 'stats']:
# These fields are stored in separate normalized tables, not as JSONB
if fieldName in ['documents', 'stats', 'logs', 'messages']:
objectFields[fieldName] = value
continue
# Check if this is a JSONB field (Dict, List, or complex types)
# Purely type-based detection - no hardcoded field names
if (fieldType == dict or
fieldType == list or
(hasattr(fieldType, '__origin__') and fieldType.__origin__ in (dict, list)) or
fieldName in ['execParameters', 'expectedDocumentFormats', 'resultDocuments']):
(hasattr(fieldType, '__origin__') and fieldType.__origin__ in (dict, list))):
# Store as JSONB - include in simple_fields for database storage
simpleFields[fieldName] = value
elif isinstance(value, (str, int, float, bool, type(None))):

View file

@ -140,6 +140,47 @@ class ComponentObjects:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def _separateObjectFields(self, model_class, data: Dict[str, Any]) -> tuple[Dict[str, Any], Dict[str, Any]]:
"""Separate simple fields from object fields based on Pydantic model structure."""
simpleFields = {}
objectFields = {}
# Get field information from the Pydantic model
modelFields = model_class.model_fields
for fieldName, value in data.items():
# Check if this field should be stored as JSONB in the database
if fieldName in modelFields:
fieldInfo = modelFields[fieldName]
# Pydantic v2 only
fieldType = fieldInfo.annotation
# Check if this is a JSONB field (Dict, List, or complex types)
# Purely type-based detection - no hardcoded field names
if (fieldType == dict or
fieldType == list or
(hasattr(fieldType, '__origin__') and fieldType.__origin__ in (dict, list))):
# Store as JSONB - include in simple_fields for database storage
simpleFields[fieldName] = value
elif isinstance(value, (str, int, float, bool, type(None))):
# Simple scalar types
simpleFields[fieldName] = value
else:
# Complex objects that should be filtered out
objectFields[fieldName] = value
else:
# Field not in model - treat as scalar if simple, otherwise filter out
# BUT: always include metadata fields (_createdBy, _createdAt, etc.) as they're handled by connector
if fieldName.startswith("_"):
# Metadata fields should be passed through to connector
simpleFields[fieldName] = value
elif isinstance(value, (str, int, float, bool, type(None))):
simpleFields[fieldName] = value
else:
objectFields[fieldName] = value
return simpleFields, objectFields
def _initRecords(self):
"""Initializes standard records in the database if they don't exist."""
try:

View file

@ -1,90 +0,0 @@
"""
Access control for Real Estate interface.
Handles user access management and permission checks.
"""
import logging
from typing import Dict, Any, List, Optional
from modules.datamodels.datamodelUam import User
logger = logging.getLogger(__name__)
class RealEstateAccess:
"""
Access control class for Real Estate interface.
Handles user access management and permission checks.
"""
def __init__(self, currentUser: User, db):
"""Initialize with user context."""
self.currentUser = currentUser
self.mandateId = currentUser.mandateId
self.userId = currentUser.id
self.roleLabels = currentUser.roleLabels or []
if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and userId are required")
self.db = db
def uam(self, model_class: type, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Unified user access management function that filters data based on user privileges.
Args:
model_class: Pydantic model class for the table
recordset: Recordset to filter based on access rules
Returns:
Filtered recordset with access control attributes
"""
filtered_records = []
# System admins see all records
if "sysadmin" in self.roleLabels:
filtered_records = recordset
# Admins see records in their mandate
elif "admin" in self.roleLabels:
filtered_records = [r for r in recordset if r.get("mandateId", "-") == self.mandateId]
# Regular users see only their records
else:
filtered_records = [
r for r in recordset
if r.get("mandateId", "-") == self.mandateId and r.get("_createdBy") == self.userId
]
# Add access control attributes
for record in filtered_records:
record["_hideView"] = False
record["_hideEdit"] = not self.canModify(model_class, record.get("id"))
record["_hideDelete"] = not self.canModify(model_class, record.get("id"))
return filtered_records
def canModify(self, model_class: type, recordId: Optional[str] = None) -> bool:
"""Checks if the current user can modify records."""
# System admins can modify all records
if "sysadmin" in self.roleLabels:
return True
if recordId is not None:
records = self.db.getRecordset(model_class, recordFilter={"id": recordId})
if not records:
return False
record = records[0]
# Admins can modify records in their mandate
if "admin" in self.roleLabels and record.get("mandateId", "-") == self.mandateId:
return True
# Regular users can modify their own records
if (record.get("mandateId", "-") == self.mandateId and
record.get("_createdBy") == self.userId):
return True
return False
else:
return True # Regular users can create records

View file

@ -21,8 +21,10 @@ from modules.datamodels.datamodelRealEstate import (
from modules.datamodels.datamodelUam import User
from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.shared.configuration import APP_CONFIG
# Import Access-Klasse aus separater Datei
from modules.interfaces.interfaceDbRealEstateAccess import RealEstateAccess
from modules.security.rbac import RbacClass
from modules.datamodels.datamodelRbac import AccessRuleContext
from modules.datamodels.datamodelUam import AccessLevel
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
logger = logging.getLogger(__name__)
@ -42,7 +44,7 @@ class RealEstateObjects:
self.currentUser = currentUser
self.userId = currentUser.id if currentUser else None
self.mandateId = currentUser.mandateId if currentUser else None
self.access = None
self.rbac = None # RBAC interface
# Initialize database
self._initializeDatabase()
@ -108,8 +110,13 @@ class RealEstateObjects:
if not self.userId or not self.mandateId:
raise ValueError("Invalid user context: id and mandateId are required")
# Initialize access control
self.access = RealEstateAccess(self.currentUser, self.db)
# Initialize RBAC interface
if not self.currentUser:
raise ValueError("User context is required for RBAC")
# Get DbApp connection for RBAC AccessRule queries
from modules.security.rootAccess import getRootDbAppConnector
dbApp = getRootDbAppConnector()
self.rbac = RbacClass(self.db, dbApp=dbApp)
# Update database context
self.db.updateContext(self.userId)
@ -118,13 +125,14 @@ class RealEstateObjects:
def createProjekt(self, projekt: Projekt) -> Projekt:
"""Create a new project."""
# Check RBAC permission
if not self.checkRbacPermission(Projekt, "create"):
raise PermissionError(f"User {self.userId} cannot create projects")
# Ensure mandateId is set
if not projekt.mandateId:
projekt.mandateId = self.mandateId
# Apply access control
self.access.uam(Projekt, [])
# Save to database - use mode='json' to ensure nested Pydantic models are serialized
self.db.recordCreate(Projekt, projekt.model_dump(mode='json'))
@ -132,30 +140,28 @@ class RealEstateObjects:
def getProjekt(self, projektId: str) -> Optional[Projekt]:
"""Get a project by ID."""
records = self.db.getRecordset(
records = getRecordsetWithRBAC(
self.db,
Projekt,
self.currentUser,
recordFilter={"id": projektId}
)
if not records:
return None
# Apply access control
filtered = self.access.uam(Projekt, records)
if not filtered:
return None
return Projekt(**filtered[0])
return Projekt(**records[0])
def getProjekte(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Projekt]:
"""Get all projects matching the filter."""
records = self.db.getRecordset(Projekt, recordFilter=recordFilter or {})
records = getRecordsetWithRBAC(
self.db,
Projekt,
self.currentUser,
recordFilter=recordFilter or {}
)
# Apply access control
filtered = self.access.uam(Projekt, records)
return [Projekt(**r) for r in filtered]
return [Projekt(**r) for r in records]
def updateProjekt(self, projektId_or_projekt: Union[str, Projekt], updateData: Optional[Dict[str, Any]] = None) -> Optional[Projekt]:
"""Update a project.
@ -180,8 +186,8 @@ class RealEstateObjects:
if hasattr(projekt, key):
setattr(projekt, key, value)
# Check if user can modify
if not self.access.canModify(Projekt, projektId):
# Check RBAC permission
if not self.checkRbacPermission(Projekt, "update", projektId):
raise PermissionError(f"User {self.userId} cannot modify project {projektId}")
# Save to database
@ -195,8 +201,8 @@ class RealEstateObjects:
if not projekt:
return False
# Check if user can modify
if not self.access.canModify(Projekt, projektId):
# Check RBAC permission
if not self.checkRbacPermission(Projekt, "delete", projektId):
raise PermissionError(f"User {self.userId} cannot delete project {projektId}")
return self.db.recordDelete(Projekt, projektId)
@ -205,10 +211,13 @@ class RealEstateObjects:
def createParzelle(self, parzelle: Parzelle) -> Parzelle:
"""Create a new plot."""
# Check RBAC permission
if not self.checkRbacPermission(Parzelle, "create"):
raise PermissionError(f"User {self.userId} cannot create plots")
if not parzelle.mandateId:
parzelle.mandateId = self.mandateId
self.access.uam(Parzelle, [])
# Use mode='json' to ensure nested Pydantic models (like GeoPolylinie) are serialized
self.db.recordCreate(Parzelle, parzelle.model_dump(mode='json'))
@ -216,20 +225,17 @@ class RealEstateObjects:
def getParzelle(self, parzelleId: str) -> Optional[Parzelle]:
"""Get a plot by ID."""
records = self.db.getRecordset(
records = getRecordsetWithRBAC(
self.db,
Parzelle,
self.currentUser,
recordFilter={"id": parzelleId}
)
if not records:
return None
filtered = self.access.uam(Parzelle, records)
if not filtered:
return None
return Parzelle(**filtered[0])
return Parzelle(**records[0])
def getParzellen(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Parzelle]:
"""Get all plots matching the filter."""
@ -243,7 +249,12 @@ class RealEstateObjects:
recordFilter = self._resolveLocationFilters(recordFilter)
records = self.db.getRecordset(Parzelle, recordFilter=recordFilter or {})
records = getRecordsetWithRBAC(
self.db,
Parzelle,
self.currentUser,
recordFilter=recordFilter or {}
)
# Fallback: If no records found and we resolved a Gemeinde name,
# try searching with the original name for backwards compatibility
@ -253,14 +264,16 @@ class RealEstateObjects:
logger.info(f"No results with resolved UUID, trying with original name '{original_gemeinde_value}'")
fallback_filter = recordFilter.copy()
fallback_filter["kontextGemeinde"] = original_gemeinde_value
records = self.db.getRecordset(Parzelle, recordFilter=fallback_filter)
records = getRecordsetWithRBAC(
self.db,
Parzelle,
self.currentUser,
recordFilter=fallback_filter
)
if records:
logger.info(f"Found {len(records)} records using original name (legacy data format)")
# Apply access control
filtered = self.access.uam(Parzelle, records)
return [Parzelle(**r) for r in filtered]
return [Parzelle(**r) for r in records]
def _resolveLocationFilters(self, recordFilter: Dict[str, Any]) -> Dict[str, Any]:
"""
@ -402,7 +415,7 @@ class RealEstateObjects:
if not parzelle:
return None
if not self.access.canModify(Parzelle, parzelleId):
if not self.checkRbacPermission(Parzelle, "update", parzelleId):
raise PermissionError(f"User {self.userId} cannot modify plot {parzelleId}")
for key, value in updateData.items():
@ -419,7 +432,7 @@ class RealEstateObjects:
if not parzelle:
return False
if not self.access.canModify(Parzelle, parzelleId):
if not self.checkRbacPermission(Parzelle, "delete", parzelleId):
raise PermissionError(f"User {self.userId} cannot delete plot {parzelleId}")
return self.db.recordDelete(Parzelle, parzelleId)
@ -428,36 +441,40 @@ class RealEstateObjects:
def createDokument(self, dokument: Dokument) -> Dokument:
"""Create a new document."""
# Check RBAC permission
if not self.checkRbacPermission(Dokument, "create"):
raise PermissionError(f"User {self.userId} cannot create documents")
if not dokument.mandateId:
dokument.mandateId = self.mandateId
self.access.uam(Dokument, [])
self.db.recordCreate(Dokument, dokument.model_dump())
return dokument
def getDokument(self, dokumentId: str) -> Optional[Dokument]:
"""Get a document by ID."""
records = self.db.getRecordset(
records = getRecordsetWithRBAC(
self.db,
Dokument,
self.currentUser,
recordFilter={"id": dokumentId}
)
if not records:
return None
filtered = self.access.uam(Dokument, records)
if not filtered:
return None
return Dokument(**filtered[0])
return Dokument(**records[0])
def getDokumente(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Dokument]:
"""Get all documents matching the filter."""
records = self.db.getRecordset(Dokument, recordFilter=recordFilter or {})
filtered = self.access.uam(Dokument, records)
return [Dokument(**r) for r in filtered]
records = getRecordsetWithRBAC(
self.db,
Dokument,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Dokument(**r) for r in records]
def updateDokument(self, dokumentId: str, updateData: Dict[str, Any]) -> Optional[Dokument]:
"""Update a document."""
@ -465,7 +482,7 @@ class RealEstateObjects:
if not dokument:
return None
if not self.access.canModify(Dokument, dokumentId):
if not self.checkRbacPermission(Dokument, "update", dokumentId):
raise PermissionError(f"User {self.userId} cannot modify document {dokumentId}")
for key, value in updateData.items():
@ -481,7 +498,7 @@ class RealEstateObjects:
if not dokument:
return False
if not self.access.canModify(Dokument, dokumentId):
if not self.checkRbacPermission(Dokument, "delete", dokumentId):
raise PermissionError(f"User {self.userId} cannot delete document {dokumentId}")
return self.db.recordDelete(Dokument, dokumentId)
@ -490,36 +507,40 @@ class RealEstateObjects:
def createGemeinde(self, gemeinde: Gemeinde) -> Gemeinde:
"""Create a new municipality."""
# Check RBAC permission
if not self.checkRbacPermission(Gemeinde, "create"):
raise PermissionError(f"User {self.userId} cannot create municipalities")
if not gemeinde.mandateId:
gemeinde.mandateId = self.mandateId
self.access.uam(Gemeinde, [])
self.db.recordCreate(Gemeinde, gemeinde.model_dump())
return gemeinde
def getGemeinde(self, gemeindeId: str) -> Optional[Gemeinde]:
"""Get a municipality by ID."""
records = self.db.getRecordset(
records = getRecordsetWithRBAC(
self.db,
Gemeinde,
self.currentUser,
recordFilter={"id": gemeindeId}
)
if not records:
return None
filtered = self.access.uam(Gemeinde, records)
if not filtered:
return None
return Gemeinde(**filtered[0])
return Gemeinde(**records[0])
def getGemeinden(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Gemeinde]:
"""Get all municipalities matching the filter."""
records = self.db.getRecordset(Gemeinde, recordFilter=recordFilter or {})
filtered = self.access.uam(Gemeinde, records)
return [Gemeinde(**r) for r in filtered]
records = getRecordsetWithRBAC(
self.db,
Gemeinde,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Gemeinde(**r) for r in records]
def updateGemeinde(self, gemeindeId: str, updateData: Dict[str, Any]) -> Optional[Gemeinde]:
"""Update a municipality."""
@ -527,7 +548,7 @@ class RealEstateObjects:
if not gemeinde:
return None
if not self.access.canModify(Gemeinde, gemeindeId):
if not self.checkRbacPermission(Gemeinde, "update", gemeindeId):
raise PermissionError(f"User {self.userId} cannot modify municipality {gemeindeId}")
for key, value in updateData.items():
@ -543,7 +564,7 @@ class RealEstateObjects:
if not gemeinde:
return False
if not self.access.canModify(Gemeinde, gemeindeId):
if not self.checkRbacPermission(Gemeinde, "delete", gemeindeId):
raise PermissionError(f"User {self.userId} cannot delete municipality {gemeindeId}")
return self.db.recordDelete(Gemeinde, gemeindeId)
@ -552,36 +573,40 @@ class RealEstateObjects:
def createKanton(self, kanton: Kanton) -> Kanton:
"""Create a new canton."""
# Check RBAC permission
if not self.checkRbacPermission(Kanton, "create"):
raise PermissionError(f"User {self.userId} cannot create cantons")
if not kanton.mandateId:
kanton.mandateId = self.mandateId
self.access.uam(Kanton, [])
self.db.recordCreate(Kanton, kanton.model_dump())
return kanton
def getKanton(self, kantonId: str) -> Optional[Kanton]:
"""Get a canton by ID."""
records = self.db.getRecordset(
records = getRecordsetWithRBAC(
self.db,
Kanton,
self.currentUser,
recordFilter={"id": kantonId}
)
if not records:
return None
filtered = self.access.uam(Kanton, records)
if not filtered:
return None
return Kanton(**filtered[0])
return Kanton(**records[0])
def getKantone(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Kanton]:
"""Get all cantons matching the filter."""
records = self.db.getRecordset(Kanton, recordFilter=recordFilter or {})
filtered = self.access.uam(Kanton, records)
return [Kanton(**r) for r in filtered]
records = getRecordsetWithRBAC(
self.db,
Kanton,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Kanton(**r) for r in records]
def updateKanton(self, kantonId: str, updateData: Dict[str, Any]) -> Optional[Kanton]:
"""Update a canton."""
@ -589,7 +614,7 @@ class RealEstateObjects:
if not kanton:
return None
if not self.access.canModify(Kanton, kantonId):
if not self.checkRbacPermission(Kanton, "update", kantonId):
raise PermissionError(f"User {self.userId} cannot modify canton {kantonId}")
for key, value in updateData.items():
@ -605,7 +630,7 @@ class RealEstateObjects:
if not kanton:
return False
if not self.access.canModify(Kanton, kantonId):
if not self.checkRbacPermission(Kanton, "delete", kantonId):
raise PermissionError(f"User {self.userId} cannot delete canton {kantonId}")
return self.db.recordDelete(Kanton, kantonId)
@ -614,36 +639,40 @@ class RealEstateObjects:
def createLand(self, land: Land) -> Land:
"""Create a new country."""
# Check RBAC permission
if not self.checkRbacPermission(Land, "create"):
raise PermissionError(f"User {self.userId} cannot create countries")
if not land.mandateId:
land.mandateId = self.mandateId
self.access.uam(Land, [])
self.db.recordCreate(Land, land.model_dump())
return land
def getLand(self, landId: str) -> Optional[Land]:
"""Get a country by ID."""
records = self.db.getRecordset(
records = getRecordsetWithRBAC(
self.db,
Land,
self.currentUser,
recordFilter={"id": landId}
)
if not records:
return None
filtered = self.access.uam(Land, records)
if not filtered:
return None
return Land(**filtered[0])
return Land(**records[0])
def getLaender(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Land]:
"""Get all countries matching the filter."""
records = self.db.getRecordset(Land, recordFilter=recordFilter or {})
filtered = self.access.uam(Land, records)
return [Land(**r) for r in filtered]
records = getRecordsetWithRBAC(
self.db,
Land,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Land(**r) for r in records]
def updateLand(self, landId: str, updateData: Dict[str, Any]) -> Optional[Land]:
"""Update a country."""
@ -651,7 +680,7 @@ class RealEstateObjects:
if not land:
return None
if not self.access.canModify(Land, landId):
if not self.checkRbacPermission(Land, "update", landId):
raise PermissionError(f"User {self.userId} cannot modify country {landId}")
for key, value in updateData.items():
@ -667,11 +696,51 @@ class RealEstateObjects:
if not land:
return False
if not self.access.canModify(Land, landId):
if not self.checkRbacPermission(Land, "delete", landId):
raise PermissionError(f"User {self.userId} cannot delete country {landId}")
return self.db.recordDelete(Land, landId)
# ===== RBAC Permission Checks =====
def checkRbacPermission(
self,
modelClass: type,
operation: str,
recordId: Optional[str] = None
) -> bool:
"""
Check RBAC permission for a specific operation on a table.
Args:
modelClass: Pydantic model class for the table
operation: Operation to check ('create', 'update', 'delete', 'read')
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
if not self.rbac or not self.currentUser:
return False
tableName = modelClass.__name__
permissions = self.rbac.getUserPermissions(
self.currentUser,
AccessRuleContext.DATA,
tableName
)
if operation == "create":
return permissions.create != AccessLevel.NONE
elif operation == "update":
return permissions.update != AccessLevel.NONE
elif operation == "delete":
return permissions.delete != AccessLevel.NONE
elif operation == "read":
return permissions.read != AccessLevel.NONE
else:
return False
# ===== Direct Query Execution (stateless) =====
def executeQuery(self, queryText: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:

View file

@ -112,10 +112,24 @@ def getRecordsetWithRBAC(
)
elif fieldType == "JSONB" and fieldName in record:
if record[fieldName] is None:
if fieldName in ["logs", "messages", "tasks", "expectedDocumentFormats", "resultDocuments"]:
record[fieldName] = []
elif fieldName in ["execParameters", "stats"]:
record[fieldName] = {}
# Generic type-based default: List types -> [], Dict types -> {}
# Interfaces handle domain-specific defaults
modelFields = modelClass.model_fields
fieldInfo = modelFields.get(fieldName)
if fieldInfo:
fieldAnnotation = fieldInfo.annotation
# Check if it's a List type
if (fieldAnnotation == list or
(hasattr(fieldAnnotation, "__origin__") and
fieldAnnotation.__origin__ is list)):
record[fieldName] = []
# Check if it's a Dict type
elif (fieldAnnotation == dict or
(hasattr(fieldAnnotation, "__origin__") and
fieldAnnotation.__origin__ is dict)):
record[fieldName] = {}
else:
record[fieldName] = None
else:
record[fieldName] = None
else:

File diff suppressed because it is too large Load diff