Kairo-Jinkendo/backend/config_service.py
Lars 53047b6c16
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 18s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 24s
AP0.4: Feature-, Prompt- und Config-Registry mit Single-Render und Entitlements-Features.
Migration 005, Registry-Sync, Placeholder-Validation, capability-geschuetzte API, Tests und Abschlussbericht v0.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 23:29:02 +02:00

209 lines
7.0 KiB
Python

"""Central configuration service — no plaintext secrets."""
from __future__ import annotations
import json
from typing import Any
from db import get_connection
from services.audit import log_audit
SECRET_VALUE_FORBIDDEN = (
"Configuration entries with is_secret=true must not store plaintext values; "
"use environment or secret management."
)
def _validate_secret_entry(*, is_secret: bool, value: Any) -> None:
if not is_secret:
return
if value not in (None, {}, "", {"ref": "env"}):
raise ValueError(SECRET_VALUE_FORBIDDEN)
def get_config(
config_key: str,
*,
scope: str = "global",
tenant_id: str | None = None,
) -> dict | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT config_key, scope, tenant_id, value, value_type, description, is_secret
FROM configuration_entries
WHERE config_key = %s AND scope = %s
AND (
(scope = 'global' AND tenant_id IS NULL)
OR (scope = 'tenant' AND tenant_id = %s::uuid)
)
""",
(config_key, scope, tenant_id),
)
row = cur.fetchone()
if not row:
return None
return {
"config_key": row[0],
"scope": row[1],
"tenant_id": str(row[2]) if row[2] else None,
"value": row[3],
"value_type": row[4],
"description": row[5],
"is_secret": row[6],
}
finally:
conn.close()
def list_configs(*, scope: str | None = None, tenant_id: str | None = None) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
if scope == "global":
cur.execute(
"""
SELECT config_key, scope, tenant_id, value, value_type, description, is_secret
FROM configuration_entries
WHERE scope = 'global' AND tenant_id IS NULL
ORDER BY config_key
"""
)
elif scope == "tenant" and tenant_id:
cur.execute(
"""
SELECT config_key, scope, tenant_id, value, value_type, description, is_secret
FROM configuration_entries
WHERE scope = 'tenant' AND tenant_id = %s::uuid
ORDER BY config_key
""",
(tenant_id,),
)
else:
cur.execute(
"""
SELECT config_key, scope, tenant_id, value, value_type, description, is_secret
FROM configuration_entries
ORDER BY scope, config_key
"""
)
rows = []
for row in cur.fetchall():
item = {
"config_key": row[0],
"scope": row[1],
"tenant_id": str(row[2]) if row[2] else None,
"value_type": row[4],
"description": row[5],
"is_secret": row[6],
}
if row[6]:
item["value"] = {"ref": "env"}
else:
item["value"] = row[3]
rows.append(item)
return rows
finally:
conn.close()
def upsert_config(
*,
config_key: str,
scope: str,
value: Any,
value_type: str = "string",
description: str = "",
is_secret: bool = False,
tenant_id: str | None = None,
user_id: str | None = None,
) -> dict:
_validate_secret_entry(is_secret=is_secret, value=value)
if scope == "tenant" and not tenant_id:
raise ValueError("tenant_id required for tenant-scoped config")
if scope == "global":
tenant_id = None
conn = get_connection()
try:
with conn.cursor() as cur:
if scope == "global":
cur.execute(
"""
INSERT INTO configuration_entries (
config_key, scope, tenant_id, value, value_type, description, is_secret
)
VALUES (%s, 'global', NULL, %s::jsonb, %s, %s, %s)
ON CONFLICT (config_key) WHERE scope = 'global' DO UPDATE SET
value = EXCLUDED.value,
value_type = EXCLUDED.value_type,
description = EXCLUDED.description,
is_secret = EXCLUDED.is_secret,
updated_at = NOW()
""",
(
config_key,
json.dumps(value if not is_secret else {"ref": "env"}),
value_type,
description,
is_secret,
),
)
else:
cur.execute(
"""
INSERT INTO configuration_entries (
config_key, scope, tenant_id, value, value_type, description, is_secret
)
VALUES (%s, 'tenant', %s::uuid, %s::jsonb, %s, %s, %s)
ON CONFLICT (config_key, tenant_id) WHERE scope = 'tenant' DO UPDATE SET
value = EXCLUDED.value,
value_type = EXCLUDED.value_type,
description = EXCLUDED.description,
is_secret = EXCLUDED.is_secret,
updated_at = NOW()
""",
(
config_key,
tenant_id,
json.dumps(value if not is_secret else {"ref": "env"}),
value_type,
description,
is_secret,
),
)
conn.commit()
finally:
conn.close()
log_audit(
"config.entry.changed",
user_id=user_id,
tenant_id=tenant_id,
details={"config_key": config_key, "scope": scope, "is_secret": is_secret},
)
result = get_config(config_key, scope=scope, tenant_id=tenant_id)
assert result is not None
return result
def sync_default_configs(defaults: list[dict]) -> int:
if not defaults:
return 0
for item in defaults:
existing = get_config(item["config_key"], scope=item.get("scope", "global"))
if existing:
continue
upsert_config(
config_key=item["config_key"],
scope=item.get("scope", "global"),
value=item["value"],
value_type=item.get("value_type", "string"),
description=item.get("description", ""),
is_secret=item.get("is_secret", False),
)
print(f"[config_service] Default configs ensured — {len(defaults)} item(s)")
return 0