fix: cleaned interfaces to work all with rbac
This commit is contained in:
parent
abbed64463
commit
5380e30f0d
12 changed files with 415 additions and 1616 deletions
|
|
@ -29,6 +29,13 @@ DB_MANAGEMENT_USER=heeshkdlby
|
||||||
DB_MANAGEMENT_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjTnJKNlJMNmEwQ0Y5dVNrR3pkZk9SQXVvLTRTNW9lQ1g3TTE5cFhBNTd5UENqWW9qdWd3NWNseWhnUHJveDJyd1Z3X1czS3VuZnAwZHBXYVNQWlZsRy12ME42NndEVlR5X3ZPdFBNNmhLYm89
|
DB_MANAGEMENT_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjTnJKNlJMNmEwQ0Y5dVNrR3pkZk9SQXVvLTRTNW9lQ1g3TTE5cFhBNTd5UENqWW9qdWd3NWNseWhnUHJveDJyd1Z3X1czS3VuZnAwZHBXYVNQWlZsRy12ME42NndEVlR5X3ZPdFBNNmhLYm89
|
||||||
DB_MANAGEMENT_PORT=5432
|
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
|
# Security Configuration
|
||||||
APP_JWT_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNUctb2RwU25iR3ZnanBOdHZhWUtIajZ1RnZzTEp4aDR0MktWRjNoeVBrY1Npd1R0VE9YVHp3M2w1cXRzbUxNaU82QUJvaDNFeVQyN05KblRWblBvbWtoT0VXbkNBbDQ5OHhwSUFnaDZGRG10Vmgtdm1YUkRsYUhFMzRVZURmSFlDTFIzVWg4MXNueDZyMGc5aVpFdWRxY3dkTExGM093ZTVUZVl5LUhGWnlRPQ==
|
APP_JWT_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNUctb2RwU25iR3ZnanBOdHZhWUtIajZ1RnZzTEp4aDR0MktWRjNoeVBrY1Npd1R0VE9YVHp3M2w1cXRzbUxNaU82QUJvaDNFeVQyN05KblRWblBvbWtoT0VXbkNBbDQ5OHhwSUFnaDZGRG10Vmgtdm1YUkRsYUhFMzRVZURmSFlDTFIzVWg4MXNueDZyMGc5aVpFdWRxY3dkTExGM093ZTVUZVl5LUhGWnlRPQ==
|
||||||
APP_TOKEN_EXPIRY=300
|
APP_TOKEN_EXPIRY=300
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ DB_MANAGEMENT_USER=gzxxmcrdhn
|
||||||
DB_MANAGEMENT_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3ZWpySThqdlVmWWd5dGxmWE91RVBsenZrQmNhSzVxbktmYzZ1RlM3cXhTMUdXRV9wX1lfLTJXLTFzeUo0R3pWLXlmUWdrZ2x6QkFlZVRXaEF6aUdRbDlzb1FfcWtub0dxSGp3OVVQWGg3enM9
|
DB_MANAGEMENT_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3ZWpySThqdlVmWWd5dGxmWE91RVBsenZrQmNhSzVxbktmYzZ1RlM3cXhTMUdXRV9wX1lfLTJXLTFzeUo0R3pWLXlmUWdrZ2x6QkFlZVRXaEF6aUdRbDlzb1FfcWtub0dxSGp3OVVQWGg3enM9
|
||||||
DB_MANAGEMENT_PORT=5432
|
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
|
# Security Configuration
|
||||||
APP_JWT_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3elhfV0Rnd2pQRjlMdkVwX1FnSmRhSzNZUlV5SVpaWXBNX1hpa2xPZGdMSWpnN2ZINHQxeGZnNHJweU5pZjlyYlY5Qm9zOUZEbl9wUEgtZHZXd1NhR19JSG9kbFU4MnFGQnllbFhRQVphRGQyNHlFVWR5VHQyUUpqN0stUmRuY2QyTi1oalczRHpLTEJqWURjZWs4YjZvT2U5YnFqcXEwdEpxV05fX05QMmtrPQ==
|
APP_JWT_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3elhfV0Rnd2pQRjlMdkVwX1FnSmRhSzNZUlV5SVpaWXBNX1hpa2xPZGdMSWpnN2ZINHQxeGZnNHJweU5pZjlyYlY5Qm9zOUZEbl9wUEgtZHZXd1NhR19JSG9kbFU4MnFGQnllbFhRQVphRGQyNHlFVWR5VHQyUUpqN0stUmRuY2QyTi1oalczRHpLTEJqWURjZWs4YjZvT2U5YnFqcXEwdEpxV05fX05QMmtrPQ==
|
||||||
APP_TOKEN_EXPIRY=300
|
APP_TOKEN_EXPIRY=300
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ def _get_model_fields(model_class) -> Dict[str, str]:
|
||||||
field_type = field_info.annotation
|
field_type = field_info.annotation
|
||||||
|
|
||||||
# Check for JSONB fields (Dict, List, or complex types)
|
# Check for JSONB fields (Dict, List, or complex types)
|
||||||
|
# Purely type-based detection - no hardcoded field names
|
||||||
if (
|
if (
|
||||||
field_type == dict
|
field_type == dict
|
||||||
or field_type == list
|
or field_type == list
|
||||||
|
|
@ -58,23 +59,7 @@ def _get_model_fields(model_class) -> Dict[str, str]:
|
||||||
hasattr(field_type, "__origin__")
|
hasattr(field_type, "__origin__")
|
||||||
and field_type.__origin__ in (dict, list)
|
and field_type.__origin__ in (dict, list)
|
||||||
)
|
)
|
||||||
or field_name
|
# Check if field type is a Pydantic BaseModel (for nested models)
|
||||||
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)
|
|
||||||
or (hasattr(field_type, "__origin__") and get_origin(field_type) is Union
|
or (hasattr(field_type, "__origin__") and get_origin(field_type) is Union
|
||||||
and any(hasattr(arg, "__bases__") and BaseModel in getattr(arg, "__bases__", ())
|
and any(hasattr(arg, "__bases__") and BaseModel in getattr(arg, "__bases__", ())
|
||||||
for arg in get_args(field_type)))
|
for arg in get_args(field_type)))
|
||||||
|
|
@ -691,23 +676,30 @@ class DatabaseConnector:
|
||||||
|
|
||||||
# Handle JSONB fields for all records
|
# Handle JSONB fields for all records
|
||||||
fields = _get_model_fields(model_class)
|
fields = _get_model_fields(model_class)
|
||||||
|
model_fields = model_class.model_fields # Get Pydantic model fields
|
||||||
for record in records:
|
for record in records:
|
||||||
for field_name, field_type in fields.items():
|
for field_name, field_type in fields.items():
|
||||||
if field_type == "JSONB" and field_name in record:
|
if field_type == "JSONB" and field_name in record:
|
||||||
if record[field_name] is None:
|
if record[field_name] is None:
|
||||||
# Convert None to appropriate default based on field name
|
# Generic type-based default: List types -> [], Dict types -> {}
|
||||||
if field_name in [
|
# Interfaces handle domain-specific defaults
|
||||||
"logs",
|
field_info = model_fields.get(field_name)
|
||||||
"messages",
|
if field_info:
|
||||||
"tasks",
|
field_annotation = field_info.annotation
|
||||||
"expectedDocumentFormats",
|
# Check if it's a List type
|
||||||
"resultDocuments",
|
if (field_annotation == list or
|
||||||
]:
|
(hasattr(field_annotation, "__origin__") and
|
||||||
|
field_annotation.__origin__ is list)):
|
||||||
record[field_name] = []
|
record[field_name] = []
|
||||||
elif field_name in ["execParameters", "stats"]:
|
# 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] = {}
|
record[field_name] = {}
|
||||||
else:
|
else:
|
||||||
record[field_name] = None
|
record[field_name] = None
|
||||||
|
else:
|
||||||
|
record[field_name] = None
|
||||||
else:
|
else:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
@ -878,6 +870,7 @@ class DatabaseConnector:
|
||||||
|
|
||||||
# Handle JSONB fields and ensure numeric types are correct
|
# Handle JSONB fields and ensure numeric types are correct
|
||||||
fields = _get_model_fields(model_class)
|
fields = _get_model_fields(model_class)
|
||||||
|
model_fields = model_class.model_fields # Get Pydantic model fields
|
||||||
for record in records:
|
for record in records:
|
||||||
for field_name, field_type in fields.items():
|
for field_name, field_type in fields.items():
|
||||||
# Ensure numeric fields (float/int) are properly typed
|
# Ensure numeric fields (float/int) are properly typed
|
||||||
|
|
@ -897,19 +890,25 @@ class DatabaseConnector:
|
||||||
)
|
)
|
||||||
elif field_type == "JSONB" and field_name in record:
|
elif field_type == "JSONB" and field_name in record:
|
||||||
if record[field_name] is None:
|
if record[field_name] is None:
|
||||||
# Convert None to appropriate default based on field name
|
# Generic type-based default: List types -> [], Dict types -> {}
|
||||||
if field_name in [
|
# Interfaces handle domain-specific defaults
|
||||||
"logs",
|
field_info = model_fields.get(field_name)
|
||||||
"messages",
|
if field_info:
|
||||||
"tasks",
|
field_annotation = field_info.annotation
|
||||||
"expectedDocumentFormats",
|
# Check if it's a List type
|
||||||
"resultDocuments",
|
if (field_annotation == list or
|
||||||
]:
|
(hasattr(field_annotation, "__origin__") and
|
||||||
|
field_annotation.__origin__ is list)):
|
||||||
record[field_name] = []
|
record[field_name] = []
|
||||||
elif field_name in ["execParameters", "stats"]:
|
# 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] = {}
|
record[field_name] = {}
|
||||||
else:
|
else:
|
||||||
record[field_name] = None
|
record[field_name] = None
|
||||||
|
else:
|
||||||
|
record[field_name] = None
|
||||||
else:
|
else:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -878,7 +878,7 @@ class SwissTopoMapServerConnector:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
x, y = coords
|
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
|
# Schritt 2: Polygon-Geometrie abrufen
|
||||||
identify_params = {
|
identify_params = {
|
||||||
|
|
|
||||||
|
|
@ -645,6 +645,54 @@ def createTableSpecificRules(db: DatabaseConnector) -> None:
|
||||||
delete=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
|
# Create all table-specific rules
|
||||||
for rule in tableRules:
|
for rule in tableRules:
|
||||||
db.recordCreate(AccessRule, rule)
|
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}
|
existingRoles = {rule.get("roleLabel") for rule in existingRules}
|
||||||
|
|
||||||
# Tables that need rules
|
# Tables that need rules
|
||||||
requiredTables = ["ChatWorkflow", "Prompt"]
|
requiredTables = ["ChatWorkflow", "Prompt", "Projekt", "Parzelle", "Dokument", "Gemeinde", "Kanton", "Land"]
|
||||||
requiredRoles = ["sysadmin", "admin", "user", "viewer"]
|
requiredRoles = ["sysadmin", "admin", "user", "viewer"]
|
||||||
|
|
||||||
newRules = []
|
newRules = []
|
||||||
|
|
@ -1005,6 +1053,53 @@ def _addMissingTableRules(db: DatabaseConnector, existingRules: List[Dict[str, A
|
||||||
update=AccessLevel.NONE,
|
update=AccessLevel.NONE,
|
||||||
delete=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
|
# Create missing rules
|
||||||
if newRules:
|
if newRules:
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,47 @@ class AppObjects:
|
||||||
logger.error(f"Failed to initialize database: {str(e)}")
|
logger.error(f"Failed to initialize database: {str(e)}")
|
||||||
raise
|
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):
|
def _initRecords(self):
|
||||||
"""Initialize standard records if they don't exist."""
|
"""Initialize standard records if they don't exist."""
|
||||||
initBootstrap(self.db)
|
initBootstrap(self.db)
|
||||||
|
|
|
||||||
|
|
@ -223,15 +223,16 @@ class ChatObjects:
|
||||||
fieldType = fieldInfo.annotation
|
fieldType = fieldInfo.annotation
|
||||||
|
|
||||||
# Always route relational/object fields to object_fields for separate handling
|
# 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
|
objectFields[fieldName] = value
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if this is a JSONB field (Dict, List, or complex types)
|
# Check if this is a JSONB field (Dict, List, or complex types)
|
||||||
|
# Purely type-based detection - no hardcoded field names
|
||||||
if (fieldType == dict or
|
if (fieldType == dict or
|
||||||
fieldType == list or
|
fieldType == list or
|
||||||
(hasattr(fieldType, '__origin__') and fieldType.__origin__ in (dict, list)) or
|
(hasattr(fieldType, '__origin__') and fieldType.__origin__ in (dict, list))):
|
||||||
fieldName in ['execParameters', 'expectedDocumentFormats', 'resultDocuments']):
|
|
||||||
# Store as JSONB - include in simple_fields for database storage
|
# Store as JSONB - include in simple_fields for database storage
|
||||||
simpleFields[fieldName] = value
|
simpleFields[fieldName] = value
|
||||||
elif isinstance(value, (str, int, float, bool, type(None))):
|
elif isinstance(value, (str, int, float, bool, type(None))):
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,47 @@ class ComponentObjects:
|
||||||
logger.error(f"Failed to initialize database: {str(e)}")
|
logger.error(f"Failed to initialize database: {str(e)}")
|
||||||
raise
|
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):
|
def _initRecords(self):
|
||||||
"""Initializes standard records in the database if they don't exist."""
|
"""Initializes standard records in the database if they don't exist."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -21,8 +21,10 @@ from modules.datamodels.datamodelRealEstate import (
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
# Import Access-Klasse aus separater Datei
|
from modules.security.rbac import RbacClass
|
||||||
from modules.interfaces.interfaceDbRealEstateAccess import RealEstateAccess
|
from modules.datamodels.datamodelRbac import AccessRuleContext
|
||||||
|
from modules.datamodels.datamodelUam import AccessLevel
|
||||||
|
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -42,7 +44,7 @@ class RealEstateObjects:
|
||||||
self.currentUser = currentUser
|
self.currentUser = currentUser
|
||||||
self.userId = currentUser.id if currentUser else None
|
self.userId = currentUser.id if currentUser else None
|
||||||
self.mandateId = currentUser.mandateId if currentUser else None
|
self.mandateId = currentUser.mandateId if currentUser else None
|
||||||
self.access = None
|
self.rbac = None # RBAC interface
|
||||||
|
|
||||||
# Initialize database
|
# Initialize database
|
||||||
self._initializeDatabase()
|
self._initializeDatabase()
|
||||||
|
|
@ -108,8 +110,13 @@ class RealEstateObjects:
|
||||||
if not self.userId or not self.mandateId:
|
if not self.userId or not self.mandateId:
|
||||||
raise ValueError("Invalid user context: id and mandateId are required")
|
raise ValueError("Invalid user context: id and mandateId are required")
|
||||||
|
|
||||||
# Initialize access control
|
# Initialize RBAC interface
|
||||||
self.access = RealEstateAccess(self.currentUser, self.db)
|
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
|
# Update database context
|
||||||
self.db.updateContext(self.userId)
|
self.db.updateContext(self.userId)
|
||||||
|
|
@ -118,13 +125,14 @@ class RealEstateObjects:
|
||||||
|
|
||||||
def createProjekt(self, projekt: Projekt) -> Projekt:
|
def createProjekt(self, projekt: Projekt) -> Projekt:
|
||||||
"""Create a new project."""
|
"""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
|
# Ensure mandateId is set
|
||||||
if not projekt.mandateId:
|
if not projekt.mandateId:
|
||||||
projekt.mandateId = self.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
|
# Save to database - use mode='json' to ensure nested Pydantic models are serialized
|
||||||
self.db.recordCreate(Projekt, projekt.model_dump(mode='json'))
|
self.db.recordCreate(Projekt, projekt.model_dump(mode='json'))
|
||||||
|
|
||||||
|
|
@ -132,30 +140,28 @@ class RealEstateObjects:
|
||||||
|
|
||||||
def getProjekt(self, projektId: str) -> Optional[Projekt]:
|
def getProjekt(self, projektId: str) -> Optional[Projekt]:
|
||||||
"""Get a project by ID."""
|
"""Get a project by ID."""
|
||||||
records = self.db.getRecordset(
|
records = getRecordsetWithRBAC(
|
||||||
|
self.db,
|
||||||
Projekt,
|
Projekt,
|
||||||
|
self.currentUser,
|
||||||
recordFilter={"id": projektId}
|
recordFilter={"id": projektId}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Apply access control
|
return Projekt(**records[0])
|
||||||
filtered = self.access.uam(Projekt, records)
|
|
||||||
|
|
||||||
if not filtered:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Projekt(**filtered[0])
|
|
||||||
|
|
||||||
def getProjekte(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Projekt]:
|
def getProjekte(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Projekt]:
|
||||||
"""Get all projects matching the filter."""
|
"""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
|
return [Projekt(**r) for r in records]
|
||||||
filtered = self.access.uam(Projekt, records)
|
|
||||||
|
|
||||||
return [Projekt(**r) for r in filtered]
|
|
||||||
|
|
||||||
def updateProjekt(self, projektId_or_projekt: Union[str, Projekt], updateData: Optional[Dict[str, Any]] = None) -> Optional[Projekt]:
|
def updateProjekt(self, projektId_or_projekt: Union[str, Projekt], updateData: Optional[Dict[str, Any]] = None) -> Optional[Projekt]:
|
||||||
"""Update a project.
|
"""Update a project.
|
||||||
|
|
@ -180,8 +186,8 @@ class RealEstateObjects:
|
||||||
if hasattr(projekt, key):
|
if hasattr(projekt, key):
|
||||||
setattr(projekt, key, value)
|
setattr(projekt, key, value)
|
||||||
|
|
||||||
# Check if user can modify
|
# Check RBAC permission
|
||||||
if not self.access.canModify(Projekt, projektId):
|
if not self.checkRbacPermission(Projekt, "update", projektId):
|
||||||
raise PermissionError(f"User {self.userId} cannot modify project {projektId}")
|
raise PermissionError(f"User {self.userId} cannot modify project {projektId}")
|
||||||
|
|
||||||
# Save to database
|
# Save to database
|
||||||
|
|
@ -195,8 +201,8 @@ class RealEstateObjects:
|
||||||
if not projekt:
|
if not projekt:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check if user can modify
|
# Check RBAC permission
|
||||||
if not self.access.canModify(Projekt, projektId):
|
if not self.checkRbacPermission(Projekt, "delete", projektId):
|
||||||
raise PermissionError(f"User {self.userId} cannot delete project {projektId}")
|
raise PermissionError(f"User {self.userId} cannot delete project {projektId}")
|
||||||
|
|
||||||
return self.db.recordDelete(Projekt, projektId)
|
return self.db.recordDelete(Projekt, projektId)
|
||||||
|
|
@ -205,10 +211,13 @@ class RealEstateObjects:
|
||||||
|
|
||||||
def createParzelle(self, parzelle: Parzelle) -> Parzelle:
|
def createParzelle(self, parzelle: Parzelle) -> Parzelle:
|
||||||
"""Create a new plot."""
|
"""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:
|
if not parzelle.mandateId:
|
||||||
parzelle.mandateId = self.mandateId
|
parzelle.mandateId = self.mandateId
|
||||||
|
|
||||||
self.access.uam(Parzelle, [])
|
|
||||||
# Use mode='json' to ensure nested Pydantic models (like GeoPolylinie) are serialized
|
# Use mode='json' to ensure nested Pydantic models (like GeoPolylinie) are serialized
|
||||||
self.db.recordCreate(Parzelle, parzelle.model_dump(mode='json'))
|
self.db.recordCreate(Parzelle, parzelle.model_dump(mode='json'))
|
||||||
|
|
||||||
|
|
@ -216,20 +225,17 @@ class RealEstateObjects:
|
||||||
|
|
||||||
def getParzelle(self, parzelleId: str) -> Optional[Parzelle]:
|
def getParzelle(self, parzelleId: str) -> Optional[Parzelle]:
|
||||||
"""Get a plot by ID."""
|
"""Get a plot by ID."""
|
||||||
records = self.db.getRecordset(
|
records = getRecordsetWithRBAC(
|
||||||
|
self.db,
|
||||||
Parzelle,
|
Parzelle,
|
||||||
|
self.currentUser,
|
||||||
recordFilter={"id": parzelleId}
|
recordFilter={"id": parzelleId}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
filtered = self.access.uam(Parzelle, records)
|
return Parzelle(**records[0])
|
||||||
|
|
||||||
if not filtered:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Parzelle(**filtered[0])
|
|
||||||
|
|
||||||
def getParzellen(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Parzelle]:
|
def getParzellen(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Parzelle]:
|
||||||
"""Get all plots matching the filter."""
|
"""Get all plots matching the filter."""
|
||||||
|
|
@ -243,7 +249,12 @@ class RealEstateObjects:
|
||||||
|
|
||||||
recordFilter = self._resolveLocationFilters(recordFilter)
|
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,
|
# Fallback: If no records found and we resolved a Gemeinde name,
|
||||||
# try searching with the original name for backwards compatibility
|
# 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}'")
|
logger.info(f"No results with resolved UUID, trying with original name '{original_gemeinde_value}'")
|
||||||
fallback_filter = recordFilter.copy()
|
fallback_filter = recordFilter.copy()
|
||||||
fallback_filter["kontextGemeinde"] = original_gemeinde_value
|
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:
|
if records:
|
||||||
logger.info(f"Found {len(records)} records using original name (legacy data format)")
|
logger.info(f"Found {len(records)} records using original name (legacy data format)")
|
||||||
|
|
||||||
# Apply access control
|
return [Parzelle(**r) for r in records]
|
||||||
filtered = self.access.uam(Parzelle, records)
|
|
||||||
|
|
||||||
return [Parzelle(**r) for r in filtered]
|
|
||||||
|
|
||||||
def _resolveLocationFilters(self, recordFilter: Dict[str, Any]) -> Dict[str, Any]:
|
def _resolveLocationFilters(self, recordFilter: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -402,7 +415,7 @@ class RealEstateObjects:
|
||||||
if not parzelle:
|
if not parzelle:
|
||||||
return None
|
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}")
|
raise PermissionError(f"User {self.userId} cannot modify plot {parzelleId}")
|
||||||
|
|
||||||
for key, value in updateData.items():
|
for key, value in updateData.items():
|
||||||
|
|
@ -419,7 +432,7 @@ class RealEstateObjects:
|
||||||
if not parzelle:
|
if not parzelle:
|
||||||
return False
|
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}")
|
raise PermissionError(f"User {self.userId} cannot delete plot {parzelleId}")
|
||||||
|
|
||||||
return self.db.recordDelete(Parzelle, parzelleId)
|
return self.db.recordDelete(Parzelle, parzelleId)
|
||||||
|
|
@ -428,36 +441,40 @@ class RealEstateObjects:
|
||||||
|
|
||||||
def createDokument(self, dokument: Dokument) -> Dokument:
|
def createDokument(self, dokument: Dokument) -> Dokument:
|
||||||
"""Create a new document."""
|
"""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:
|
if not dokument.mandateId:
|
||||||
dokument.mandateId = self.mandateId
|
dokument.mandateId = self.mandateId
|
||||||
|
|
||||||
self.access.uam(Dokument, [])
|
|
||||||
self.db.recordCreate(Dokument, dokument.model_dump())
|
self.db.recordCreate(Dokument, dokument.model_dump())
|
||||||
|
|
||||||
return dokument
|
return dokument
|
||||||
|
|
||||||
def getDokument(self, dokumentId: str) -> Optional[Dokument]:
|
def getDokument(self, dokumentId: str) -> Optional[Dokument]:
|
||||||
"""Get a document by ID."""
|
"""Get a document by ID."""
|
||||||
records = self.db.getRecordset(
|
records = getRecordsetWithRBAC(
|
||||||
|
self.db,
|
||||||
Dokument,
|
Dokument,
|
||||||
|
self.currentUser,
|
||||||
recordFilter={"id": dokumentId}
|
recordFilter={"id": dokumentId}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
filtered = self.access.uam(Dokument, records)
|
return Dokument(**records[0])
|
||||||
|
|
||||||
if not filtered:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Dokument(**filtered[0])
|
|
||||||
|
|
||||||
def getDokumente(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Dokument]:
|
def getDokumente(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Dokument]:
|
||||||
"""Get all documents matching the filter."""
|
"""Get all documents matching the filter."""
|
||||||
records = self.db.getRecordset(Dokument, recordFilter=recordFilter or {})
|
records = getRecordsetWithRBAC(
|
||||||
filtered = self.access.uam(Dokument, records)
|
self.db,
|
||||||
return [Dokument(**r) for r in filtered]
|
Dokument,
|
||||||
|
self.currentUser,
|
||||||
|
recordFilter=recordFilter or {}
|
||||||
|
)
|
||||||
|
return [Dokument(**r) for r in records]
|
||||||
|
|
||||||
def updateDokument(self, dokumentId: str, updateData: Dict[str, Any]) -> Optional[Dokument]:
|
def updateDokument(self, dokumentId: str, updateData: Dict[str, Any]) -> Optional[Dokument]:
|
||||||
"""Update a document."""
|
"""Update a document."""
|
||||||
|
|
@ -465,7 +482,7 @@ class RealEstateObjects:
|
||||||
if not dokument:
|
if not dokument:
|
||||||
return None
|
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}")
|
raise PermissionError(f"User {self.userId} cannot modify document {dokumentId}")
|
||||||
|
|
||||||
for key, value in updateData.items():
|
for key, value in updateData.items():
|
||||||
|
|
@ -481,7 +498,7 @@ class RealEstateObjects:
|
||||||
if not dokument:
|
if not dokument:
|
||||||
return False
|
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}")
|
raise PermissionError(f"User {self.userId} cannot delete document {dokumentId}")
|
||||||
|
|
||||||
return self.db.recordDelete(Dokument, dokumentId)
|
return self.db.recordDelete(Dokument, dokumentId)
|
||||||
|
|
@ -490,36 +507,40 @@ class RealEstateObjects:
|
||||||
|
|
||||||
def createGemeinde(self, gemeinde: Gemeinde) -> Gemeinde:
|
def createGemeinde(self, gemeinde: Gemeinde) -> Gemeinde:
|
||||||
"""Create a new municipality."""
|
"""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:
|
if not gemeinde.mandateId:
|
||||||
gemeinde.mandateId = self.mandateId
|
gemeinde.mandateId = self.mandateId
|
||||||
|
|
||||||
self.access.uam(Gemeinde, [])
|
|
||||||
self.db.recordCreate(Gemeinde, gemeinde.model_dump())
|
self.db.recordCreate(Gemeinde, gemeinde.model_dump())
|
||||||
|
|
||||||
return gemeinde
|
return gemeinde
|
||||||
|
|
||||||
def getGemeinde(self, gemeindeId: str) -> Optional[Gemeinde]:
|
def getGemeinde(self, gemeindeId: str) -> Optional[Gemeinde]:
|
||||||
"""Get a municipality by ID."""
|
"""Get a municipality by ID."""
|
||||||
records = self.db.getRecordset(
|
records = getRecordsetWithRBAC(
|
||||||
|
self.db,
|
||||||
Gemeinde,
|
Gemeinde,
|
||||||
|
self.currentUser,
|
||||||
recordFilter={"id": gemeindeId}
|
recordFilter={"id": gemeindeId}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
filtered = self.access.uam(Gemeinde, records)
|
return Gemeinde(**records[0])
|
||||||
|
|
||||||
if not filtered:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Gemeinde(**filtered[0])
|
|
||||||
|
|
||||||
def getGemeinden(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Gemeinde]:
|
def getGemeinden(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Gemeinde]:
|
||||||
"""Get all municipalities matching the filter."""
|
"""Get all municipalities matching the filter."""
|
||||||
records = self.db.getRecordset(Gemeinde, recordFilter=recordFilter or {})
|
records = getRecordsetWithRBAC(
|
||||||
filtered = self.access.uam(Gemeinde, records)
|
self.db,
|
||||||
return [Gemeinde(**r) for r in filtered]
|
Gemeinde,
|
||||||
|
self.currentUser,
|
||||||
|
recordFilter=recordFilter or {}
|
||||||
|
)
|
||||||
|
return [Gemeinde(**r) for r in records]
|
||||||
|
|
||||||
def updateGemeinde(self, gemeindeId: str, updateData: Dict[str, Any]) -> Optional[Gemeinde]:
|
def updateGemeinde(self, gemeindeId: str, updateData: Dict[str, Any]) -> Optional[Gemeinde]:
|
||||||
"""Update a municipality."""
|
"""Update a municipality."""
|
||||||
|
|
@ -527,7 +548,7 @@ class RealEstateObjects:
|
||||||
if not gemeinde:
|
if not gemeinde:
|
||||||
return None
|
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}")
|
raise PermissionError(f"User {self.userId} cannot modify municipality {gemeindeId}")
|
||||||
|
|
||||||
for key, value in updateData.items():
|
for key, value in updateData.items():
|
||||||
|
|
@ -543,7 +564,7 @@ class RealEstateObjects:
|
||||||
if not gemeinde:
|
if not gemeinde:
|
||||||
return False
|
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}")
|
raise PermissionError(f"User {self.userId} cannot delete municipality {gemeindeId}")
|
||||||
|
|
||||||
return self.db.recordDelete(Gemeinde, gemeindeId)
|
return self.db.recordDelete(Gemeinde, gemeindeId)
|
||||||
|
|
@ -552,36 +573,40 @@ class RealEstateObjects:
|
||||||
|
|
||||||
def createKanton(self, kanton: Kanton) -> Kanton:
|
def createKanton(self, kanton: Kanton) -> Kanton:
|
||||||
"""Create a new canton."""
|
"""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:
|
if not kanton.mandateId:
|
||||||
kanton.mandateId = self.mandateId
|
kanton.mandateId = self.mandateId
|
||||||
|
|
||||||
self.access.uam(Kanton, [])
|
|
||||||
self.db.recordCreate(Kanton, kanton.model_dump())
|
self.db.recordCreate(Kanton, kanton.model_dump())
|
||||||
|
|
||||||
return kanton
|
return kanton
|
||||||
|
|
||||||
def getKanton(self, kantonId: str) -> Optional[Kanton]:
|
def getKanton(self, kantonId: str) -> Optional[Kanton]:
|
||||||
"""Get a canton by ID."""
|
"""Get a canton by ID."""
|
||||||
records = self.db.getRecordset(
|
records = getRecordsetWithRBAC(
|
||||||
|
self.db,
|
||||||
Kanton,
|
Kanton,
|
||||||
|
self.currentUser,
|
||||||
recordFilter={"id": kantonId}
|
recordFilter={"id": kantonId}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
filtered = self.access.uam(Kanton, records)
|
return Kanton(**records[0])
|
||||||
|
|
||||||
if not filtered:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Kanton(**filtered[0])
|
|
||||||
|
|
||||||
def getKantone(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Kanton]:
|
def getKantone(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Kanton]:
|
||||||
"""Get all cantons matching the filter."""
|
"""Get all cantons matching the filter."""
|
||||||
records = self.db.getRecordset(Kanton, recordFilter=recordFilter or {})
|
records = getRecordsetWithRBAC(
|
||||||
filtered = self.access.uam(Kanton, records)
|
self.db,
|
||||||
return [Kanton(**r) for r in filtered]
|
Kanton,
|
||||||
|
self.currentUser,
|
||||||
|
recordFilter=recordFilter or {}
|
||||||
|
)
|
||||||
|
return [Kanton(**r) for r in records]
|
||||||
|
|
||||||
def updateKanton(self, kantonId: str, updateData: Dict[str, Any]) -> Optional[Kanton]:
|
def updateKanton(self, kantonId: str, updateData: Dict[str, Any]) -> Optional[Kanton]:
|
||||||
"""Update a canton."""
|
"""Update a canton."""
|
||||||
|
|
@ -589,7 +614,7 @@ class RealEstateObjects:
|
||||||
if not kanton:
|
if not kanton:
|
||||||
return None
|
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}")
|
raise PermissionError(f"User {self.userId} cannot modify canton {kantonId}")
|
||||||
|
|
||||||
for key, value in updateData.items():
|
for key, value in updateData.items():
|
||||||
|
|
@ -605,7 +630,7 @@ class RealEstateObjects:
|
||||||
if not kanton:
|
if not kanton:
|
||||||
return False
|
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}")
|
raise PermissionError(f"User {self.userId} cannot delete canton {kantonId}")
|
||||||
|
|
||||||
return self.db.recordDelete(Kanton, kantonId)
|
return self.db.recordDelete(Kanton, kantonId)
|
||||||
|
|
@ -614,36 +639,40 @@ class RealEstateObjects:
|
||||||
|
|
||||||
def createLand(self, land: Land) -> Land:
|
def createLand(self, land: Land) -> Land:
|
||||||
"""Create a new country."""
|
"""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:
|
if not land.mandateId:
|
||||||
land.mandateId = self.mandateId
|
land.mandateId = self.mandateId
|
||||||
|
|
||||||
self.access.uam(Land, [])
|
|
||||||
self.db.recordCreate(Land, land.model_dump())
|
self.db.recordCreate(Land, land.model_dump())
|
||||||
|
|
||||||
return land
|
return land
|
||||||
|
|
||||||
def getLand(self, landId: str) -> Optional[Land]:
|
def getLand(self, landId: str) -> Optional[Land]:
|
||||||
"""Get a country by ID."""
|
"""Get a country by ID."""
|
||||||
records = self.db.getRecordset(
|
records = getRecordsetWithRBAC(
|
||||||
|
self.db,
|
||||||
Land,
|
Land,
|
||||||
|
self.currentUser,
|
||||||
recordFilter={"id": landId}
|
recordFilter={"id": landId}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
filtered = self.access.uam(Land, records)
|
return Land(**records[0])
|
||||||
|
|
||||||
if not filtered:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Land(**filtered[0])
|
|
||||||
|
|
||||||
def getLaender(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Land]:
|
def getLaender(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Land]:
|
||||||
"""Get all countries matching the filter."""
|
"""Get all countries matching the filter."""
|
||||||
records = self.db.getRecordset(Land, recordFilter=recordFilter or {})
|
records = getRecordsetWithRBAC(
|
||||||
filtered = self.access.uam(Land, records)
|
self.db,
|
||||||
return [Land(**r) for r in filtered]
|
Land,
|
||||||
|
self.currentUser,
|
||||||
|
recordFilter=recordFilter or {}
|
||||||
|
)
|
||||||
|
return [Land(**r) for r in records]
|
||||||
|
|
||||||
def updateLand(self, landId: str, updateData: Dict[str, Any]) -> Optional[Land]:
|
def updateLand(self, landId: str, updateData: Dict[str, Any]) -> Optional[Land]:
|
||||||
"""Update a country."""
|
"""Update a country."""
|
||||||
|
|
@ -651,7 +680,7 @@ class RealEstateObjects:
|
||||||
if not land:
|
if not land:
|
||||||
return None
|
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}")
|
raise PermissionError(f"User {self.userId} cannot modify country {landId}")
|
||||||
|
|
||||||
for key, value in updateData.items():
|
for key, value in updateData.items():
|
||||||
|
|
@ -667,11 +696,51 @@ class RealEstateObjects:
|
||||||
if not land:
|
if not land:
|
||||||
return False
|
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}")
|
raise PermissionError(f"User {self.userId} cannot delete country {landId}")
|
||||||
|
|
||||||
return self.db.recordDelete(Land, 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) =====
|
# ===== Direct Query Execution (stateless) =====
|
||||||
|
|
||||||
def executeQuery(self, queryText: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
def executeQuery(self, queryText: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,26 @@ def getRecordsetWithRBAC(
|
||||||
)
|
)
|
||||||
elif fieldType == "JSONB" and fieldName in record:
|
elif fieldType == "JSONB" and fieldName in record:
|
||||||
if record[fieldName] is None:
|
if record[fieldName] is None:
|
||||||
if fieldName in ["logs", "messages", "tasks", "expectedDocumentFormats", "resultDocuments"]:
|
# 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] = []
|
record[fieldName] = []
|
||||||
elif fieldName in ["execParameters", "stats"]:
|
# Check if it's a Dict type
|
||||||
|
elif (fieldAnnotation == dict or
|
||||||
|
(hasattr(fieldAnnotation, "__origin__") and
|
||||||
|
fieldAnnotation.__origin__ is dict)):
|
||||||
record[fieldName] = {}
|
record[fieldName] = {}
|
||||||
else:
|
else:
|
||||||
record[fieldName] = None
|
record[fieldName] = None
|
||||||
|
else:
|
||||||
|
record[fieldName] = None
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if isinstance(record[fieldName], str):
|
if isinstance(record[fieldName], str):
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue