Sicherheitsmodell¶
Provisa setzt ein mehrschichtiges Sicherheitsmodell über alle Abfragesprachen (GraphQL, SQL, Cypher) und alle Transporte (REST, gRPC, Arrow Flight, JDBC, WebSocket) hinweg durch. (REQ-001, REQ-266) Governance wird einheitlich angewendet — es gibt keinen Abfragepfad, der sie umgeht. (REQ-002, REQ-266)
Die Schichten gelten der Reihe nach. Eine Anfrage muss jede Schicht durchlaufen, bevor die nächste ausgewertet wird.
Schichtenmodell¶
Schicht 0 — Introspektionsfilterung¶
Das Schema und der Katalog, die einer Rolle angezeigt werden, enthalten nur die Tabellen aus ihrer domain_access-Liste und die Spalten, die die spaltenspezifischen visible_to-Regeln erfüllen. (REQ-039) Objekte außerhalb des Zugriffsbereichs einer Rolle sind zum Zeitpunkt der Erkennung unsichtbar — sie können weder abgefragt noch autovervollständigt noch als existent abgeleitet werden. (REQ-039) Dies gilt für das GraphQL-Schema, den SQL-Katalog und den Schema-Browser des Abfrage-Editors. (REQ-039, REQ-363)
Siehe Schema-Sichtbarkeit.
Schicht 1 — Öffentlicher Zugriff¶
Tabellen in Domänen ohne domain_access-Beschränkung sind für alle authentifizierten Identitäten ohne zusätzliche Konfiguration sichtbar. Keine Reibungsverluste für tatsächlich öffentliche Daten.
Schicht 2 — Domänenzugriff¶
Jede Rolle besitzt eine domain_access-Liste von Domänen-IDs. Eine Abfrage, die eine Tabelle außerhalb dieser Domänen berührt, wird vor der Ausführung abgelehnt. (REQ-038, REQ-039) Dies ist die grobkörnige Eigentumsgrenze — eine HR-Rolle kann unabhängig davon, wie das SQL geschrieben ist, nicht auf Finanztabellen zugreifen. (REQ-002)
Siehe Rechtemodell.
Schicht 3 — Sicherheit auf Zeilenebene¶
Nachdem der Domänenzugriff bestätigt wurde, werden WHERE-Prädikate pro Tabelle und Rolle zur Ausführungszeit in jedes SELECT injiziert. (REQ-041, REQ-263) Die Prädikate werden gegen die Rohdaten ausgewertet. Ein Regionalleiter, der eine gemeinsam genutzte Bestelltabelle abfragt, sieht selbst bei einem SELECT * nur die Zeilen seiner Region. (REQ-264)
Siehe Sicherheit auf Zeilenebene (RLS).
Schicht 4 — Spaltensichtbarkeit und Maskierung¶
Spalten mit einer visible_to-Liste, die die anfragende Rolle ausschließt, werden aus der Abfrageausgabe entfernt. (REQ-040, REQ-263) Bei Spalten mit einer Maskierungsregel werden die Werte ersetzt — durch Redaktion per regulärem Ausdruck, Ersetzung durch eine Konstante oder Kürzung — bevor die Ergebnisse den Server verlassen. (REQ-263) Die Maskierung gilt in allen Abfragesprachen und Ausgabeformaten. (REQ-263)
Siehe Spaltenberechtigungsmodell und Maskierung auf Spaltenebene.
Schicht 5 — Prädikatsschutz¶
Maskierte Spalten werden in WHERE- und HAVING-Klauseln abgelehnt. (REQ-263) Ohne diesen Schutz könnte ein Aufrufer den unmaskierten Wert durch binäre Suche in einem Filter ableiten, selbst wenn die Ausgabe maskiert ist. Die Ablehnung erfolgt zum Zeitpunkt des Abfrage-Parsens, vor der Ausführung. (REQ-531)
Governance der Beziehungen (V002)¶
JOIN-Bedingungen in SQL müssen einer registrierten, genehmigten Beziehung zwischen Tabellen entsprechen. (REQ-001) Nicht genehmigte Joins werden abgelehnt. Jede Beziehung trägt einen für Menschen lesbaren Grund und eine Beschreibung — eine Orientierungshilfe sowohl für Benutzer als auch für autonome Agenten, warum ein Traversierungspfad existiert. Dies ist eine Governance-Richtlinie, keine harte Sicherheitsgrenze: Die Schichten 2–5 gelten unabhängig von der Join-Struktur, sodass eine gezielte Umgehung keine Daten offenlegt, die die Rolle nicht auch über zwei separate Abfragen erreichen könnte. Umgehungsversuche werden protokolliert und sind auditierbar.
Umgehungsmechanismen — V002 kann nur umgangen werden, wenn zwei unabhängige Bedingungen gleichzeitig erfüllt sind:
- Rollen-Flag —
relationship_guard: falsein der Rollendefinition (Standard:true). [tool-verified:provisa/core/models.py:349] - Opt-out pro Abfrage — das SQL enthält den Kommentar
--relationship-guard=false. [tool-verified:provisa/compiler/params.py:80]
Beide müssen vorhanden sein. Das Rollen-Flag allein umgeht V002 nicht; der Kommentar allein umgeht V002 nicht.
GraphQL-Pfad — V002 wird bei GraphQL-Abfragen bedingungslos übersprungen. In SDL definierte Beziehungen sind konstruktionsbedingt bereits genehmigt; die Prüfung ist redundant und wird nicht angewendet. [tool-verified: provisa/api/data/endpoint.py:468]
SQL- und Cypher-Pfade — V002 ist standardmäßig aktiv. Sowohl endpoint_dev.py als auch cypher_router.py wenden die Zwei-Bedingungen-Prüfung an, bevor validate_sql aufgerufen wird. [tool-verified: provisa/api/data/endpoint_dev.py:127, provisa/api/rest/cypher_router.py:260]
pgwire-Pfad — dieselbe Zwei-Bedingungen-Prüfung wie bei SQL. Der Kommentar --relationship-guard=false wird vor der Ausführung aus der Abfrage entfernt; er erreicht die Datenbank nicht. [tool-verified: provisa/pgwire/_pipeline.py:60]
Diese Schichten kombinieren sich. Eine Rolle mit Domänenzugriff, RLS und maskierten Spalten hat alle fünf Einschränkungen gleichzeitig aktiv. Das Hinzufügen einer neuen Datenquelle, Spalte oder Beziehung erfordert keine Aktualisierung jeder einzelnen Regel — jede Schicht wird unabhängig konfiguriert und gilt automatisch für jede Abfrage, die gesteuerte Objekte berührt.
Rechtemodell¶
Unabhängig zugewiesene Fähigkeiten mit optionaler Rollenhierarchie über parent_role_id. admin gewährt alle. (REQ-042)
| Fähigkeit | Beschreibung |
|---|---|
source_registration |
Datenquellen registrieren |
table_registration |
Tabellen, Spalten registrieren |
create_relationship |
Fremdschlüsselbeziehungen definieren |
access_config |
RLS, Maskierung konfigurieren |
query_development |
Abfragen ausführen |
write |
Registrierte Mutationen aufrufen (grobkörnige Schranke; siehe Mutationsautorisierung) |
full_results |
Stichprobenlimits umgehen |
ignore_relationships |
Beziehungs-Governance umgehen (V002) |
admin |
Superuser — gewährt alle Fähigkeiten |
Rollenvererbung¶
Rollen können Fähigkeiten und Domänenzugriff von einer übergeordneten Rolle über parent_role_id erben. (REQ-215) Die Hierarchie wird beim Start abgeflacht — untergeordnete Rollen führen die Fähigkeiten und den Domänenzugriff ihrer übergeordneten Rolle mit ihren eigenen zusammen. (REQ-215)
roles:
- id: basic_user
capabilities: [query_development]
domain_access: [public]
- id: analyst
capabilities: [full_results]
domain_access: [sales, analytics]
parent_role_id: basic_user # inherits query_development + public domain
Spaltenberechtigungsmodell¶
Jede Spalte verfügt über ein Berechtigungsmodell mit vier Feldern, das Lese-, Schreib- und Maskierungszugriff pro Rolle steuert. (REQ-042, REQ-249)
Drei Sichtbarkeitsstufen¶
| Stufe | Bedingung | Ergebnis |
|---|---|---|
| Verborgen | Rolle nicht in visible_to |
Spalte fehlt im GraphQL-SDL |
| Maskiert | Rolle in visible_to, hat Maskierungsregel, Rolle nicht in unmasked_to |
Spalte sichtbar, aber Daten in SQL maskiert |
| Unmaskiert | Rolle in visible_to UND Rolle in unmasked_to (oder keine Maskierungsregel) |
Vollständiger Lesezugriff |
Schreibberechtigungen¶
| Feld | Leer bedeutet | Zweck |
|---|---|---|
visible_to |
Alle Rollen können lesen | Steuert, wer die Spalte sieht (maskiert oder unmaskiert) |
unmasked_to |
Keine Rolle sieht unmaskierte Werte | Steuert, wer die Maskierung umgeht |
writable_by |
Keine Rolle kann schreiben | Steuert, wer ändern darf (INSERT/UPDATE) |
Die Schreibberechtigung wird in der Mutationspipeline durchgesetzt. Eine Rolle, die nicht in writable_by enthalten ist, erhält beim Versuch, in eine eingeschränkte Spalte zu schreiben, einen 403-Fehler. (REQ-033, REQ-034)
Beispiel¶
columns:
- name: email
visible_to: [admin, analyst, viewer]
writable_by: [admin]
unmasked_to: [admin]
mask_type: regex
mask_pattern: "(.).*@"
mask_replace: "$1***@"
- name: salary
visible_to: [admin, hr]
writable_by: [hr]
unmasked_to: [admin, hr]
mask_type: constant
mask_value: "0"
- name: created_at
visible_to: [] # all can read
writable_by: [] # nobody can write (auto-set)
In diesem Beispiel:
email: admin siehtalice@example.comund kann bearbeiten; analyst/viewer sehena***@example.comsalary: admin und hr sehen den echten Wert; hr kann bearbeiten; alle anderen Rollen sehen die Spalte überhaupt nichtcreated_at: alle können lesen, niemand kann schreiben
Mutationsautorisierung¶
Registrierte Mutationen (Remote-GraphQL, OpenAPI, gRPC, Hasura) unterliegen zwei unabhängigen Prüfungen. (REQ-867, REQ-868) Eine Rolle darf eine Mutation nur aufrufen, wenn sie die globale Fähigkeit write besitzt UND in der writable_by-Liste dieser Mutation aufgeführt ist. (REQ-868) Ein leeres writable_by bedeutet standardmäßige Ablehnung — keine Rolle kann sie aufrufen. (REQ-867)
Mutationen werden vertraglich als Schreibvorgänge klassifiziert, nicht durch die Angabe des Aufrufers. (REQ-869) Ein SELECT, das auf eine Funktion vom Mutationstyp verweist, wird zu einem Schreibvorgang hochgestuft und unterliegt derselben Zwei-Schranken-Prüfung, sodass ein Aufrufer eine Mutation nicht als Lesevorgang tarnen kann. (REQ-869) Eine Neuklassifizierung einer Mutation als lesesicher erfordert die Fähigkeit access_config und wird als Governance-Entscheidung protokolliert; es gibt kein Opt-out pro Anfrage. (REQ-870)
Schema-Sichtbarkeit¶
Rollenspezifische GraphQL-Schemas verbergen nicht autorisierte Inhalte: (REQ-039)
- Domänenzugriff: Die Rolle sieht Tabellen nur in ihren
domain_access-Domänen ("*"= alle) (REQ-039) - Spaltensichtbarkeit: Spalten, die nicht in
visible_tofür eine Rolle enthalten sind, werden aus dem SDL ausgelassen (REQ-039) - Nicht autorisierte Tabellen/Spalten erscheinen nicht im Schema (REQ-039)
Sicherheit auf Zeilenebene (RLS)¶
Injektion von SQL-WHERE-Klauseln pro Tabelle und Rolle. Wird nach der Kompilierung, vor der Ausführung angewendet. (REQ-041, REQ-263)
rls_rules:
- table_id: orders
role_id: analyst
filter: "region = current_setting('provisa.user_region')"
Der Filter wird per UND-Verknüpfung in die WHERE-Klausel der Abfrage eingefügt. Funktioniert sowohl für Abfragen als auch für Mutationen (UPDATE/DELETE). (REQ-035, REQ-041)
Maskierung auf Spaltenebene¶
Die Maskierung wird einmal pro Spalte definiert — sie ist eine Eigenschaft der Spalte, nicht der Rolle. Das Feld unmasked_to steuert, welche Rollen sie umgehen. (REQ-249)
| Maskierungstyp | Unterstützte Typen | SQL-Ausdruck |
|---|---|---|
regex |
Zeichenkette (varchar, char, text) | REGEXP_REPLACE(col, pattern, replace) |
constant |
Beliebig | Literalwert (NULL, 0, benutzerdefiniert) |
truncate |
Datum/Timestamp | DATE_TRUNC(precision, col) |
Die Maskierung wird in die SQL-SELECT-Projektion verlagert — die Datenbank liefert maskierte Daten zurück. (REQ-263) Unmaskierte Daten gelangen für maskierte Rollen nie über die Leitung. (REQ-263) Maskierte Spalten werden außerdem in WHERE- und HAVING-Klauseln blockiert (Prädikatsschutz der Schicht 5), um eine Ableitung des unmaskierten Werts durch Filterung zu verhindern. (REQ-263, REQ-531)
Stichprobenerhebung¶
Alle Rollen sehen stichprobenartige Ergebnisse (Standard: 100 Zeilen), sofern sie nicht über die Fähigkeit full_results verfügen. (REQ-554) Gesteuert über die Umgebungsvariable PROVISA_SAMPLE_SIZE. (REQ-554)
Audit-Protokollierung¶
Jede Abfrage, die ein Domänen-Asset berührt, wird im Nur-Anhängen-Protokoll query_audit_log erfasst. (REQ-596, REQ-613) Jede Zeile erfasst tenant_id, user_id, role_id, einen SHA-256-Hash des Abfragetexts, table_ids, source, status_code, duration_ms und logged_at. (REQ-596) Der Abfragetext wird niemals im Klartext gespeichert — nur sein Hash. (REQ-596)
Das Protokoll ist auf Datenbankebene nur anhängend: PostgreSQL-Regeln blockieren DELETE und UPDATE. (REQ-596, REQ-613) Zwei Indizes — (tenant_id, logged_at) und (user_id, logged_at) — unterstützen mandantenbezogene und benutzerbezogene Compliance-Abfragen über Zeiträume. (REQ-596, REQ-613)
Wenn die Verschlüsselung aktiviert ist, wird die Spalte mit dem Hash des Abfragetexts verschlüsselt gespeichert und nur bei autorisierten Administratorzugriffen entschlüsselt. (REQ-689)
Ratenbegrenzung¶
Ratenlimits pro Rolle werden in provisa.yaml konfiguriert: maximale Anfragen pro Sekunde, maximale Anzahl gleichzeitiger SSE-Abonnements und maximale Anzahl gleichzeitiger Arrow-Flight-Streams. (REQ-369) Die Limits werden auf der API-Schicht vor der Kompilierung oder Ausführung durchgesetzt; Anfragen über dem Limit werden mit HTTP 429 und einem Retry-After-Header abgelehnt. (REQ-369)
Der NL-Abfragedienst (POST /query/nl) hat ein unabhängiges Limit über nl.rate_limit (Anfragen pro Minute pro Rolle). Anfragen über dem Limit werden abgelehnt, bevor ein LLM-Aufruf erfolgt. (REQ-370)
Der Zustand der Ratenbegrenzung liegt in Redis (cache.redis_url) als gleitender Fensterzähler vor — kein Zustand pro Instanz — sodass die Limits über alle horizontal skalierten Provisa-Instanzen hinweg gelten. (REQ-371)
Authentifizierung¶
Austauschbare Authentifizierungsanbieter: (REQ-120)
| Anbieter | Token-Typ | Anwendungsfall |
|---|---|---|
none |
X-Provisa-Role-Header | Entwicklung |
firebase |
Firebase-ID-Token | Produktion |
keycloak |
Keycloak-JWT | Unternehmen |
oauth |
OIDC-JWT | PingFed, Okta, Azure AD, Auth0 |
simple |
bcrypt + JWT | Tests |
Rollenzuordnung: Identitätsansprüche (Claims) → Provisa-Rolle über konfigurierbare Regeln. (REQ-120) Das Feld assignments_source bestimmt, woher die Rollenzuweisungen stammen: claims liest sie aus den Claims des JWT-Tokens (Standard), provisa liest sie aus dem internen Zuweisungsspeicher von Provisa. (REQ-551)
Ein in provisa.yaml konfigurierter Superuser (Benutzername plus ein Passwort aus einem Umgebungssecret) erhält unabhängig vom konfigurierten Anbieter immer die Admin-Rolle und alle Fähigkeiten — ein Bootstrap-Pfad für die Ersteinrichtung. (REQ-125)
ABAC-Genehmigungs-Hook¶
Ein optionaler externer Richtlinien-Hook, der vor der Ausführung der Abfrage ausgelöst wird. (REQ-203) Bei entsprechender Konfiguration ruft Provisa Ihre Policy-Engine mit der Benutzeridentität, den Rollen, den Tabellen, den Spalten und der Operation auf. Die Antwort bestimmt, ob die Abfrage fortgesetzt wird. (REQ-203)
Geltungsbereich¶
Der Hook wird nur ausgelöst, wenn die Abfrage eine Tabelle oder Quelle im festgelegten Geltungsbereich berührt — kein Overhead für alles andere. (REQ-204)
| Konfiguration | Effekt |
|---|---|
auth.approval_hook.scope: all |
Jede Abfrage löst den Hook aus |
sources[].approval_hook: true |
Alle Tabellen dieser Quelle lösen den Hook aus |
tables[].approval_hook: true |
Diese Tabelle löst den Hook aus |
Protokolle¶
Drei Transporte werden unterstützt: (REQ-246)
| Typ | Anwendungsfall | Konfigurationsfeld |
|---|---|---|
webhook |
Jeder HTTP-fähige Policy-Dienst (OPA, benutzerdefiniert) | url |
unix_socket |
OPA oder Policy-Sidecar auf derselben Maschine | socket_path + url |
grpc |
Hochdurchsatz-Policy-Dienst am selben Standort | url (Host:Port) |
Der gRPC-Transport verwendet den Vertrag provisa.auth.ApprovalService, der in provisa/auth/approval.proto definiert ist. Implementieren Sie diesen Dienst in Ihrer Policy-Engine: (REQ-246)
service ApprovalService {
rpc Evaluate (ApprovalRequest) returns (ApprovalResponse);
}
message ApprovalRequest {
string user = 1;
repeated string roles = 2;
repeated string tables = 3;
repeated string columns = 4;
string operation = 5;
}
message ApprovalResponse {
bool approved = 1;
string reason = 2;
}
Der gRPC-Kanal ist dauerhaft — ein Kanal pro Provisa-Instanz, der für alle Aufrufe an diesen Hook-Endpunkt wiederverwendet wird. (REQ-555)
Anfrage / Antwort¶
Alle drei Transporte übertragen dieselbe Nutzlast: (REQ-246)
| Feld | Typ | Beschreibung |
|---|---|---|
user |
string | Identität des authentifizierten Benutzers |
roles |
string[] | Provisa-Rollen des Benutzers |
tables |
string[] | In der Abfrage referenzierte Tabellen-IDs |
columns |
string[] | In der Abfrage ausgewählte Spalten |
operation |
string | "query" oder "mutation" |
Die Transporte Webhook und Unix-Socket tauschen JSON aus. Die Antwort muss approved (bool) enthalten und optional reason (string). (REQ-246)
Timeout und Fallback¶
auth:
approval_hook:
type: grpc # webhook | grpc | unix_socket
url: "localhost:50051"
timeout_ms: 500 # default 5000
fallback: deny # allow | deny — applied on timeout or error
scope: "" # "" = use per-table/per-source flags; "all" = every query
Bei einem Timeout oder Transportfehler greift die fallback-Richtlinie. (REQ-247) Ein Circuit Breaker (Standard: öffnet nach 5 aufeinanderfolgenden Fehlern, halb offen nach 30 s) verhindert kaskadierende Ausfälle durch einen langsamen Hook-Endpunkt. (REQ-556)
Konfigurationsbeispiel¶
auth:
approval_hook:
type: webhook
url: "http://opa.internal:8181/v1/data/provisa/allow"
timeout_ms: 300
fallback: deny
sources:
- id: analytics_pg
approval_hook: true # all tables on this source require hook approval
tables:
- id: salary_data
approval_hook: true # this table always requires hook approval
Secrets¶
Anmeldedaten verwenden die Syntax ${env:VAR_NAME}, die zur Laufzeit aufgelöst wird. (REQ-557) Passwörter werden niemals in der Konfigurationsdatenbank gespeichert. (REQ-557)