Zum Inhalt

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=TRUE per CHECK-Constraint erzwungen.
  • external_eligible — normal zubuchbar pro Tenant (ticket_escalation, confluence).
  • internal_only — reserviert für zukünftige AVS-interne Tools. Aktuell nicht zuweisbarPOST /tenants/{id}/modules liefert 400. Werden nur für vendor-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;
  • actiontenant_module.assigned oder tenant_module.revoked
  • entity_id — Format <tenant_uuid>:<module_id>
  • actor_roleoperator | vendor | tenant
  • actor_userpreferred_username aus dem Token
  • actor_keycloak_id — UUID des Subjects (NULL für Service-Accounts ohne UUID-sub)
  • details — JSONB mit before/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=TRUETrue - scope='internal_only' → immer False - Unbekannter module_keyFalse

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.