Coverage for src/lumen/api/tenant_settings.py: 90%
71 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
1"""
2This router defines routes for managing tenant-specific configuration settings.
3It is primarily used by the admin UI to adjust server behavior per tenant.
4"""
6from __future__ import annotations
8import json
9from typing import Any, Literal
11import structlog
12from fastapi import APIRouter, Response
13from fastapi.responses import JSONResponse
14from pydantic import BaseModel
16from ..audit.events import build_audit_event
17from ..auth.enforcement import ScopeEnforcerDep
18from ..config import get_settings
19from ..db.pool import get_pool
20from ..db.repository import ResourceRepository
21from ..search.registry import get_registry
22from .deps import TenantSettingsDep
24logger = structlog.get_logger(__name__)
26router = APIRouter(dependencies=[ScopeEnforcerDep])
29class TenantSettingsUpdate(BaseModel):
30 create_on_update: bool | None = None
31 reference_integrity: Literal["strict", "lenient", "disabled"] | None = None
32 validation_strictness: Literal["strict", "lenient"] | None = None
33 terminology_strictness: Literal["strict", "lenient"] | None = None
34 timezone_strictness: Literal["strict", "lenient", "disabled"] | None = None
35 profile_validation_mode: Literal["tolerant", "declared", "enforced"] | None = None
36 enforced_profiles: dict[str, list[str]] | None = None
37 terminology_pre_expansion_enabled: bool | None = None
38 ignore_unknown_search_parameters: bool | None = None
41@router.get("/tenant_settings")
42async def get_settings_endpoint(
43 tenant_settings: TenantSettingsDep,
44) -> Response:
45 resolved = {
46 "create_on_update": tenant_settings.create_on_update,
47 "reference_integrity": tenant_settings.reference_integrity,
48 "validation_strictness": tenant_settings.validation_strictness,
49 "terminology_strictness": tenant_settings.terminology_strictness,
50 "timezone_strictness": tenant_settings.timezone_strictness,
51 "profile_validation_mode": tenant_settings.profile_validation_mode,
52 "enforced_profiles": tenant_settings.enforced_profiles,
53 "terminology_pre_expansion_enabled": tenant_settings.terminology_pre_expansion_enabled,
54 "ignore_unknown_search_parameters": tenant_settings.ignore_unknown_search_parameters,
55 }
56 return JSONResponse(content=resolved)
59@router.put("/tenant_settings")
60async def update_settings_endpoint(
61 body: TenantSettingsUpdate,
62 tenant_settings: TenantSettingsDep,
63) -> Response:
64 new_settings: dict[str, Any] = {}
65 if body.create_on_update is not None:
66 new_settings["create_on_update"] = body.create_on_update
67 if body.reference_integrity is not None:
68 new_settings["reference_integrity"] = body.reference_integrity
69 if body.validation_strictness is not None:
70 new_settings["validation_strictness"] = body.validation_strictness
71 if body.terminology_strictness is not None:
72 new_settings["terminology_strictness"] = body.terminology_strictness
73 if body.timezone_strictness is not None:
74 new_settings["timezone_strictness"] = body.timezone_strictness
75 if body.profile_validation_mode is not None:
76 new_settings["profile_validation_mode"] = body.profile_validation_mode
77 if body.enforced_profiles is not None:
78 new_settings["enforced_profiles"] = body.enforced_profiles
79 if body.terminology_pre_expansion_enabled is not None:
80 new_settings["terminology_pre_expansion_enabled"] = body.terminology_pre_expansion_enabled
81 if body.ignore_unknown_search_parameters is not None:
82 new_settings["ignore_unknown_search_parameters"] = body.ignore_unknown_search_parameters
84 pool = get_pool()
85 async with pool.acquire() as conn:
86 if new_settings:
87 await conn.execute(
88 """
89 INSERT INTO tenant_settings (tenant_id, settings)
90 VALUES ($1, $2::jsonb)
91 ON CONFLICT (tenant_id) DO UPDATE
92 SET settings = EXCLUDED.settings
93 """,
94 tenant_settings.tenant_id,
95 json.dumps(new_settings),
96 )
97 logger.info("tenant_settings.updated", new_settings=new_settings)
99 # Generate AuditEvent
100 from uuid import uuid4
102 audit_id = uuid4().hex
103 audit_event = build_audit_event("U", "TenantSettings", "global")
104 audit_event["id"] = audit_id
105 await ResourceRepository(conn, get_registry()).create(
106 tenant_settings.tenant_id, "AuditEvent", audit_id, audit_event
107 )
108 else:
109 await conn.execute(
110 "DELETE FROM tenant_settings WHERE tenant_id = $1",
111 tenant_settings.tenant_id,
112 )
113 logger.info("tenant_settings.cleared")
115 # Generate AuditEvent
116 from uuid import uuid4
118 audit_id = uuid4().hex
119 audit_event = build_audit_event("D", "TenantSettings", "global")
120 audit_event["id"] = audit_id
121 await ResourceRepository(conn, get_registry()).create(
122 tenant_settings.tenant_id, "AuditEvent", audit_id, audit_event
123 )
125 global_settings = get_settings()
126 resolved = {
127 "create_on_update": new_settings.get(
128 "create_on_update", global_settings.tenant.create_on_update
129 ),
130 "reference_integrity": new_settings.get(
131 "reference_integrity", global_settings.tenant.reference_integrity
132 ),
133 "validation_strictness": new_settings.get(
134 "validation_strictness", global_settings.tenant.validation_strictness
135 ),
136 "terminology_strictness": new_settings.get(
137 "terminology_strictness", global_settings.tenant.terminology_strictness
138 ),
139 "timezone_strictness": new_settings.get(
140 "timezone_strictness", global_settings.tenant.timezone_strictness
141 ),
142 "profile_validation_mode": new_settings.get(
143 "profile_validation_mode", global_settings.tenant.profile_validation_mode
144 ),
145 "enforced_profiles": new_settings.get(
146 "enforced_profiles", global_settings.tenant.enforced_profiles
147 ),
148 "terminology_pre_expansion_enabled": new_settings.get(
149 "terminology_pre_expansion_enabled",
150 global_settings.tenant.terminology_pre_expansion_enabled,
151 ),
152 "ignore_unknown_search_parameters": new_settings.get(
153 "ignore_unknown_search_parameters",
154 global_settings.tenant.ignore_unknown_search_parameters,
155 ),
156 }
157 return JSONResponse(content=resolved)