Plattform-Module — Registry & Zuweisung¶
Runbook für Block 5 (kora-platform v1.0.0). Beschreibt das einstufige Modul-Freischaltungs-Modell nach Fundament §8 und die zugehörigen Operationen: neue Module registrieren, an Tenants zuweisen, widerrufen, Audit prüfen.
Keine UI in Block 5
Die Operator-UI für Modul-Verwaltung kommt in Block 6. Bis dahin
laufen alle Operationen über curl (dokumentiert unten) oder SQL.
Datenmodell¶
Zwei Tabellen, Schema in alembic/platform/versions/0006_platform_modules.py:
| Tabelle | Zweck | RLS |
|---|---|---|
platform_modules |
Globaler Modul-Katalog (id = Slug, scope, is_always_on) |
nein (plattformweit) |
tenant_modules |
Pro-Tenant-Zuweisung (tenant_id, module_id, is_enabled) |
ja (force+enable) |
Scope-Werte (§8.2, Konzept v5):
core— immer aktiv, nicht zuweisbar (z.B.chatbot).is_always_on=TRUEper CHECK-Constraint erzwungen.external_eligible— normal zubuchbar pro Tenant (ticket_escalation,confluence).internal_only— reserviert für zukünftige AVS-interne Tools. Aktuell nicht zuweisbar —POST /tenants/{id}/modulesliefert 400. Werden nur fürvendor-Scope in der Katalogliste angezeigt.
Konzept-Abweichung — dokumentiert: Fundament §8.1 beschreibt die
Zuweisung als JSONB-Array in tenant_packages.enabled_modules. Block 5
implementiert es als eigenständige tenant_modules-Tabelle. Vorteile:
Per-Row-Audit-Trail, enabled_by/enabled_at-Granularität,
RLS-Policies. Migration zu Array-Form jederzeit möglich, wenn
tenant_packages als Einheit (Tier + Quota + Module) dran ist.
Seed-Daten einspielen¶
Erstinbetriebnahme (einmal pro Deployment):
docker exec -it kora-platform-postgres psql -U kora_platform -d kora_platform <<'SQL'
INSERT INTO platform_modules (id, name, version, scope, is_always_on) VALUES
('chatbot', 'Chatbot-Kern', '1.0.0', 'core', TRUE),
('ticket_escalation', 'Ticket-Eskalation', '1.0.0', 'external_eligible', FALSE),
('confluence', 'Confluence-Anbindung', '1.0.0', 'external_eligible', FALSE),
('internal_analytics', 'Interne Analytik', '1.0.0', 'internal_only', FALSE)
ON CONFLICT (id) DO NOTHING;
SQL
Oder programmgesteuert via kora_platform.services.module_seeds.ensure_seed_modules(session) (idempotent).
Neues Modul registrieren¶
Module außerhalb der 4 Seeds brauchen aktuell einen direkten SQL-Insert (Registrations-Endpunkt kommt mit Block 6):
INSERT INTO platform_modules (id, name, version, scope, is_always_on)
VALUES ('ms_integration', 'MS Integration', '1.0.0', 'external_eligible', FALSE);
Wichtig: scope='core' erzwingt is_always_on=TRUE per CHECK-Constraint
(core_is_always_on). Verstoß → Constraint-Violation.
Modul einem Tenant zuweisen¶
OPERATOR_TOKEN=... # per ./scripts/gen-test-tokens.sh oder SSO-Login
TENANT_ID=11111111-1111-1111-1111-111111111111
curl -X POST \
-H "Authorization: Bearer $OPERATOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"module_id": "ticket_escalation"}' \
http://localhost:8280/api/v1/tenants/$TENANT_ID/modules
# HTTP 201 Created
Idempotent: Mehrfach-Aufrufe setzen is_enabled=TRUE und aktualisieren
enabled_by, kein Fehler.
Modul entziehen¶
curl -X DELETE \
-H "Authorization: Bearer $OPERATOR_TOKEN" \
http://localhost:8280/api/v1/tenants/$TENANT_ID/modules/ticket_escalation
# HTTP 204 No Content
Auch idempotent — kein Fehler, wenn die Zuweisung nicht existiert.
Modul-Sicht pro Tenant abrufen¶
# Operator / Vendor: alle Tenants
curl -H "Authorization: Bearer $OPERATOR_TOKEN" \
http://localhost:8280/api/v1/tenants/$TENANT_ID/modules
# Tenant: nur eigener Tenant (Cross-Access → 403)
curl -H "Authorization: Bearer $TENANT_TOKEN" \
http://localhost:8280/api/v1/tenants/$TENANT_ID/modules
Modul-Katalog abrufen¶
# Operator: core + external_eligible (ohne internal_only)
curl -H "Authorization: Bearer $OPERATOR_TOKEN" \
http://localhost:8280/api/v1/platform/modules
# Vendor: alles inkl. internal_only
curl -H "Authorization: Bearer $VENDOR_TOKEN" \
http://localhost:8280/api/v1/platform/modules
Audit-Log-Query¶
Jede Mutation (Assign/Revoke) schreibt in derselben Transaktion einen
Eintrag in platform_audit_log. Audit-Fail = HTTP 503, kompletter
Rollback (Block-3-Security-over-Availability-Pattern).
SELECT occurred_at, actor_user, actor_role, action, entity_id,
details
FROM platform_audit_log
WHERE entity_type = 'tenant_module'
ORDER BY occurred_at DESC
LIMIT 20;
action—tenant_module.assignedodertenant_module.revokedentity_id— Format<tenant_uuid>:<module_id>actor_role—operator|vendor|tenantactor_user—preferred_usernameaus dem Tokenactor_keycloak_id— UUID des Subjects (NULL für Service-Accounts ohne UUID-sub)details— JSONB mitbefore/after-Deltas
Zentrale Gate-Funktion is_module_enabled¶
Wenn eine Service-Klasse wissen muss, ob ein Modul für einen Tenant aktiv ist:
from kora_platform.services.module_service import ModuleService
service = ModuleService(session)
if await service.is_module_enabled(tenant_id, "ticket_escalation"):
# Feature ausführen
...
Single-SQL-Query mit EXISTS-Join über platform_modules +
tenant_modules. Index-getragen. Kein In-Memory-Cache in v1.0.0 —
verschoben auf Block 6 / Performance-Tuning.
Gate-Logik:
- scope='core' + is_always_on=TRUE → immer True
- scope='external_eligible' + tenant_modules.is_enabled=TRUE → True
- scope='internal_only' → immer False
- Unbekannter module_key → False
Troubleshooting¶
| Symptom | Ursache | Abhilfe |
|---|---|---|
400 {"detail":"module ... is core (always-on, not assignable)"} |
Core-Module sind implicit aktiv und nicht explizit zuweisbar | module_id auf ein external_eligible-Modul ändern |
400 {"detail":"module ... is internal_only (not assignable to tenants)"} |
Internal-Module sind reserviert für zukünftige AVS-interne Tools | bis Block 6 nicht vorgesehen |
404 {"detail":"module_not_found: ..."} |
Slug-Typo oder Modul nicht geseedet | GET /api/v1/platform/modules zum Abgleich |
403 {"detail":"not_authorized_for_tenant"} |
Tenant-User versucht auf fremden Tenant zuzugreifen | Nur Operator/Vendor dürfen cross-tenant |
503 {"detail":"audit_unavailable"} |
Audit-Log-DB-Write gescheitert, Rollback | kora-platform-postgres-Health prüfen, platform_audit_log-Tabelle intakt? |
RLS-Schutz (Defense-in-Depth)¶
tenant_modules hat ROW LEVEL SECURITY ENABLE + FORCE mit Policy
tenant_isolation_tenant_modules (USING + WITH CHECK auf
app.current_tenant_id). Das heißt:
- App-Session (Tenant-Scope) sieht nur eigene Rows — unabhängig von der Query
- App-Session kann keine fremden
tenant_id-INSERTs durchführen - BYPASSRLS-Rollen (
kora_platform_vendor, Admin) umgehen die Policy
Integration-Tests (tests/integration/test_tenant_modules_isolation.py)
verifizieren alle drei Aspekte.