DB-Rollen und Berechtigungen
Status: umgesetzt (2026-10-26, Migration
008_role_grants.sql). Massgebend sind die Grants indb/db-setup.sql(Abschnitt Roles & Grants). Dieses Dokument listet dieselben Rechte mit Begründung.db/tests/grants.sqlprüft eine Datenbank genau gegen diese Liste.
Jeder Dienst verbindet sich mit einer eigenen Login-Rolle. Eine Rolle hat nur die
Rechte, die ihr Code braucht: pro Tabelle, und bei UPDATE pro Spalte, wenn der
Dienst nur einzelne Felder ändert.
Grundsätze
Abschnitt betitelt „Grundsätze“- Keine Pauschalrechte. Es gibt kein
GRANT … ON ALL TABLESund keineDEFAULT PRIVILEGES. Eine neue Tabelle ist für alle Dienste unsichtbar, bis sie eigene Grants bekommt (siehe Änderungen). - Eine Tabelle hat einen Eigentümer-Dienst. Er legt die Zeilen an. Andere
Dienste lesen oder ändern nur einzelne Spalten, die ihnen gehören (z.B. setzt der
Writer
app_peerreviews.state = 'closed', die AppView verlängertgrace_until). DELETEnur, wo der Code löscht. Das betrifft nur den Taxonomie-Neuaufbau im Indexer und den Embedding-Cache im Writer. Sonst wird weich gelöscht (deleted = true).- Register mit Spaltenrechten. Die verschlüsselten Passwörter der
Community-Konten (
pw_ciphertext,pw_nonce) lesen nur Writer und Registrar. - Regeln über
SECURITY DEFINER-Funktionen.participation_checkundpeer_review_settingslesen das Register im Namen des Aufrufers. Indexer, AppView und Writer brauchen dafür keinen Lesezugriff aufregistry.participantsoderregistry.clients. - Owner
atvotoist die DBA-Rolle (POSTGRES_USERdes Postgres-Pods). Kein Dienst verwendet sie. Sie spielt Setup und Migrationen ein.
| Rolle | Dienst | Secret (Variable) | Aufgabe |
|---|---|---|---|
indexer |
Indexer | indexer-secrets (INDEXER_POSTGRES_URL) |
Firehose in die app_*-Tabellen projizieren |
appview |
AppView | appview-secrets (APPVIEW_POSTGRES_URL) |
Lese-API, Lesezustand (gesehen, Peer-Review-Check-in) |
writer |
Writer | writer-secrets (APPVIEW_POSTGRES_URL) |
Annahmeschleuse, Peer Review zuteilen/abschliessen, Crosspost, Übersetzungen, Embeddings |
registrar |
Registrar | registrar-secrets (APPVIEW_POSTGRES_URL) |
Admin-API: Teilnehmer, Dossiers, Client-Einstellungen |
Alle vier Dienst-Rollen haben CONNECT auf atvoto und USAGE auf public
und registry. PUBLIC hat kein CONNECT. Die Rolle calculator ist seit
Migration 014 entfernt, die Rolle ozone seit Migration 016.
Übersicht
Abschnitt betitelt „Übersicht“R = SELECT, I = INSERT, U = UPDATE (ganze Zeile), U(…) = UPDATE nur
dieser Spalten, D = DELETE, – = kein Zugriff.
Schema public
Abschnitt betitelt „Schema public“| Tabelle | indexer | appview | writer | registrar |
|---|---|---|---|---|
app_dossier_meta |
R | R | R | R I U |
indexer_cursors |
R I U | – | – | – |
app_arguments |
R I U | R | R, U(peerreview_status, indexed_at, bsky_post_uri, bsky_post_cid, bsky_post_removed) |
R, U(title, body, translations, bsky_post_removed, content_purged_at) |
app_acceptance_queue |
R I | – | R, U(status, reason, updated_at) |
– |
app_likes |
R I U | R | – | R |
app_comments |
R I U | R | R | R, U(title, text, content_purged_at) |
app_comment_translations |
R I U | R | R | – |
app_taxonomy_node |
R I U D | R | R, U(langs, translations, translation_status) |
– |
app_taxonomy_membership |
R I U D | R | – | – |
app_held_contributions |
R I U D | – | – | – |
app_taxonomy_snapshot |
R I | – | – | – |
app_peerreviews |
R I U | R, U(grace_until) |
R, U(state, closed_at, result_uri, result_cid, approvals, rejections) |
R |
app_peerreview_invitations |
R I | R, U(checked_in_at, last_activity_at) |
R I | – |
app_peerreview_responses |
R I | R | R | R |
app_content_creations |
– | R | R I | – |
app_activity_seen |
– | R I | – | – |
app_profiles |
R I U | R | – | R |
app_embeddings |
– | R | R I U D | – |
labels |
R | R | – | R I D |
labels_active (View) |
R | R | – | R |
Schema registry
Abschnitt betitelt „Schema registry“| Objekt | indexer | appview | writer | registrar |
|---|---|---|---|---|
clients |
– | – | – | R, U(participation_policy, peer_review) |
participation_policy_log |
– | – | – | R I |
peer_review_log |
– | – | – | R I |
participants |
– | – | – | R I U |
community_accounts |
R(did, dossier_rkey) |
R(did, dossier_rkey) |
R (inkl. pw_*) |
R I |
v_eligible_participants (View) |
– | – | R | – |
Sequenzen
Abschnitt betitelt „Sequenzen“| Sequenz | Rolle |
|---|---|
app_acceptance_queue_id_seq, app_taxonomy_node_id_seq |
indexer |
app_content_creations_id_seq |
writer |
registry.participation_policy_log_id_seq, registry.peer_review_log_id_seq |
registrar |
labels_seq_seq |
registrar |
Funktionen (EXECUTE)
Abschnitt betitelt „Funktionen (EXECUTE)“| Funktion | Rollen | Art |
|---|---|---|
registry.participation_check(did, dossier, action) |
indexer, appview, writer | SECURITY DEFINER: liest Register und Client-Regeln |
registry.peer_review_settings(dossier) |
indexer | SECURITY DEFINER: liest clients.peer_review |
app_response_gate(argument, reviewer) |
writer | SECURITY INVOKER: braucht SELECT auf app_peerreviews, app_arguments, app_peerreview_invitations |
app_label_active(subject, labeler) |
indexer | SECURITY INVOKER: ist das neueste Label nicht negiert? |
app_refresh_hidden(uri, did, labeler) |
indexer | SECURITY INVOKER: die Regel für hidden; schreibt nur in Tabellen des Indexers |
registry.assurance_rank(text) |
alle (PUBLIC) | reine Hilfsfunktion ohne Datenzugriff |
PUBLIC darf nur registry.assurance_rank ausführen. Die Trigger-Funktion
app_taxonomy_node_reset_translations behält das Standardrecht. Postgres prüft
bei Triggern kein EXECUTE.
Begründung pro Rolle
Abschnitt betitelt „Begründung pro Rolle“indexer
Abschnitt betitelt „indexer“Projiziert die Records aus der Firehose und ist Eigentümer fast aller
app_*-Tabellen. Code: services/indexer/src/db.js.
- R I U auf
app_arguments,app_comments,app_likes,app_comment_translations,app_peerreviews,indexer_cursors: Upserts (ON CONFLICT DO UPDATE), Zähler (like_count,comment_count), Soft-Delete, Peer-Review-Übergänge (provisional_closed, Abschluss aus dem Result-Record). Der Bluesky-Poller setztbsky_reply_count. - R I auf
app_acceptance_queue,app_taxonomy_snapshot,app_peerreview_invitations,app_peerreview_responses: nur einfügen (ON CONFLICT DO NOTHING). Peer-Review-Daten werden nie geändert oder gelöscht. - R I U D auf
app_taxonomy_node,app_taxonomy_membership: Der Snapshot baut den Baum eines Dossiers neu auf und löscht dabei Knoten und Zuordnungen, die nicht mehr vorkommen. - R I U D auf
app_held_contributions: Kommentare und Bewertungen, die die Phase der Vorlage zurückhält; nach dem Nachholen wird die Zeile gelöscht. - R auf
app_dossier_meta: Status-Gate (nuropenprojiziert Beiträge). - registry:
community_accounts(did, dossier_rkey), um Community-Repos zu erkennen.participation_checkfür das Gate,peer_review_settingsbeim Anlegen eines User-Arguments. - R auf
labels,labels_activeund EXECUTE aufapp_refresh_hidden: Nur der Indexer leitet aus Labels das Flaghiddenab (MODERATION.md, Lesemodell).
appview
Abschnitt betitelt „appview“Öffentliche Lese-API. Code: services/appview/src/routes/participation/.
- R auf alle Tabellen, die die API ausliefert (Argumente, Kommentare,
Bewertungen, Übersetzungen, Profile, Taxonomie, Peer Review), auf
app_content_creations(Kontingent,quota.py) und aufapp_dossier_meta(Status inpermissions.py). - R I auf
app_activity_seen: „gesehen“-Markierungen (activity.py). - U(
checked_in_at,last_activity_at) aufapp_peerreview_invitations: Check-in und Aktivität im Peer Review (reviews.py). - U(
grace_until) aufapp_peerreviews: verlängert die Nachfrist, solange jemand tippt. Das Spaltenrecht erlaubt auchSELECT … FOR UPDATEauf die Zeile. - registry:
community_accounts(did, dossier_rkey)undparticipation_check(permissions.py). - R auf
app_embeddings: Duplikat im Peer Review undargument.findSimilarrechnen die Ähnlichkeit per SQL; den Vektor eines Entwurfs liefert der Writer (/internal/embed). - R auf
labels,labels_active: öffentliche Auslieferung (queryLabels). Die AppView hält keinen Signaturschlüssel und kann keine Labels schreiben. - Kein
DELETE.
Interne Schreibseite. Code: services/writer/src/
ohne registrar/.
- R auf
app_dossier_meta: Status-Gate. Den Status ändert nur der Registrar. - R I U D auf
app_embeddings: Embeddings der Argumente, ein regenerierbarer Cache (embedding/backfill.py). - R, U(
status,reason,updated_at) aufapp_acceptance_queue: Zeilen beanspruchen (FOR UPDATE SKIP LOCKED) und abschliessen (atproto/acceptance.py). - R I auf
app_content_creations: Kontingent-Ledger (acceptance.py). - R I auf
app_peerreview_invitations: Einladungen sofort eintragen, ohne auf den Indexer zu warten (arguments/peer_review_assign.py). - R, U(
state,closed_at,result_uri,result_cid,approvals,rejections) aufapp_peerreviews: Nachfrist schliessen, Ergebnis eintragen (arguments/peer_review_finalize.py). - R, U(
peerreview_status,indexed_at,bsky_post_uri,bsky_post_cid,bsky_post_removed) aufapp_arguments: Ergebnis des Peer Reviews (peer_review_finalize.py), Crosspost und Abgleich der Spiegelposts (atproto/crosspost.py). Titel, Text und Quelle kann der Writer nicht ändern. - R, U(
langs,translations,translation_status) aufapp_taxonomy_node: Übersetzung der Themen (translation/translator.py). Argumente und Kommentare übersetzt er über das PDS, nicht direkt in der DB. - R auf
app_arguments,app_comments,app_comment_translations,app_peerreview_responses: Eingaben für Annahme, Zuteilung, Übersetzung. - registry:
community_accountsganz (inkl.pw_*, um in Community-Repos zu schreiben),v_eligible_participants,participation_check.app_response_gatefür Peer-Review-Antworten.
registrar
Abschnitt betitelt „registrar“Admin-API für Clients. Code: services/writer/src/registrar/.
- R I U auf
app_dossier_meta:createDossier,updateDossier(Metadaten und Status). - R I auf
registry.community_accounts: legt das Community-Konto eines Dossiers an, inkl. verschlüsseltem Passwort. - R I U auf
registry.participants:createAccount,revokeParticipant, Prüfstufe ändern. - R auf
registry.clients(Client-Key prüfen,handle_domain), U(participation_policy,peer_review) für die Client-Einstellungen, dazu R I auf die beiden Änderungsprotokolle. - R I D auf
labels, R auflabels_active, dazu die Sequenz: signierte Labels speichern; gelöscht wird nur mit dem Inhalt oder dem Konto (VO § 9 Abs. 1). - R auf
app_arguments,app_comments,app_likes,app_peerreviews,app_peerreview_responses: Daten zum Fall (moderation.getSubject,getThread,listRecent). R aufapp_profiles: Anzeigename der Autorin oder des Autors. - U(
title,body,translations,bsky_post_removed,content_purged_at) aufapp_argumentsund U(title,text,content_purged_at) aufapp_comments: nur für die rechtlich verlangte Löschung (purgeContent). - Kein Zugriff auf die übrigen
app_*-Tabellen.
Was damit nicht mehr geht
Abschnitt betitelt „Was damit nicht mehr geht“Beispiele, die vor Migration 008 möglich waren:
| Rolle | Versuch | Ergebnis |
|---|---|---|
| writer | UPDATE app_dossier_meta SET status = … |
verweigert |
| writer | UPDATE app_arguments SET title = … |
verweigert |
| writer | UPDATE app_peerreviews SET quorum = … |
verweigert |
| writer | SELECT … FROM registry.participants |
verweigert |
| appview | DELETE FROM app_comments … |
verweigert |
| appview | UPDATE app_peerreviews SET state = 'closed' |
verweigert |
| appview | SELECT pw_ciphertext FROM registry.community_accounts |
verweigert |
| indexer | UPDATE app_dossier_meta …, DELETE FROM app_arguments … |
verweigert |
Entfernt wurden ausserdem Rechte, die kein Code nutzte: AppView auf
app_embeddings, Indexer auf registry.v_eligible_participants, die Spalte
handle von community_accounts für Indexer und AppView.
Grenzen
Abschnitt betitelt „Grenzen“- Der Indexer darf die projizierten Tabellen ganz ändern, also z.B. auch
app_arguments.peerreview_status. Er bildet die Firehose ab, und die Firehose ist die Quelle dieser Daten. Wer den Indexer kontrolliert, kann die Projektion verfälschen, aber nicht die Records auf dem PDS. Ein Neuaufbau des Index stellt die Projektion wieder her. - Der Registrar darf alle Spalten von
app_dossier_metaändern, auchclient_id. Dass ein Client nur eigene Dossiers ändert, prüft der Code. - Postgres kennt keine Zeilenrechte ohne RLS. Welche Zeilen ein Dienst ändert, begrenzt weiterhin der Code (z.B. AppView: nur die eigene Einladung).
Änderungen
Abschnitt betitelt „Änderungen“Braucht ein Dienst ein neues Recht oder gibt es eine neue Tabelle:
- Grant in
db/db-setup.sqlim Abschnitt Roles & Grants ergänzen (frische Installationen). - Dieselben Grants in eine neue, idempotente Migration (Konvention). Eine Migration, die eine Tabelle anlegt, vergibt die Grants gleich mit.
- Dieses Dokument nachführen (Übersicht und Begründung).
- Die Soll-Liste in
db/tests/grants.sqlnachführen und den Test laufen lassen.
ALL TABLES und DEFAULT PRIVILEGES nicht verwenden. Der Test schlägt fehl,
wenn eine Dienst-Rolle Default-Privileges hat.
# Ist-Rechte gegen die Soll-Liste (als Owner, hinterlässt nichts)psql -h localhost -p 5497 -U atvoto -d atvoto -v ON_ERROR_STOP=1 -f db/tests/grants.sql# → NOTICE: grants: ok, sonst Liste "missing" / "extra"-- Rechte einer Tabelle von Hand ansehen\dp app_peerreviews-- Spaltenrechte einer RolleSELECT table_schema, table_name, column_name, privilege_typeFROM information_schema.column_privileges WHERE grantee = 'writer';