The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.2 KiB
Sicherheitsprüfung
Der Stand vom Juli 2026 ist überholt
Die damalige Prüfung untersuchte einen Dienstschlüssel, der den Zeilenschutz
aushebelte, einen Cookie-Adapter der abgelösten Plattform und ein
Anmeldemodell über auth.users. Alle drei Bestandteile gibt es nicht
mehr. Ihre Bewertungen stehen deshalb nicht mehr hier — ein Befund über eine
Datei, die entfernt wurde, sagt nichts über den heutigen Zustand, verleitet
aber dazu, ihn für geprüft zu halten.
Was nachweislich weg ist:
| Damals geprüft | Heute |
|---|---|
SUPABASE_SERVICE_ROLE_KEY |
Ersatzlos entfallen. Es gibt keinen Zugang, der den Zeilenschutz umgeht. |
Postgres-Rolle service_role |
Nur noch eine Platzhalterrolle ohne Anmelderecht, damit alte Migrationen abspielbar bleiben. |
| Cookie-Adapter der Plattform | Ersetzt durch Auth.js gegen Entra ID. |
auth.users |
Ersetzt durch app_users; kein Fremdschlüssel zeigt mehr nach auth. |
| Browser-Anbindung an die Datenbank | Entfallen. Der Browser spricht ausschließlich mit dieser Anwendung. |
Eine neue Prüfung des heutigen Aufbaus steht aus. Bis dahin gilt: geprüft ist, was unten steht, weil Tests es abdecken — nicht das Übrige.
Was strukturell gilt und durch Tests abgedeckt ist
Der Zeilenschutz ist die Schranke, nicht die Oberfläche. Jede Tabelle hat ihn aktiv, dazu 67 Regeln. Ein Sicherheitsnetz in der Datenbank schaltet ihn für neu angelegte Tabellen selbsttätig ein, damit eine vergessene Tabelle nicht offen steht.
Es gibt genau einen Weg an die Datenbank. withUser() setzt den
Sitzungskontext in derselben Transaktion wie die Abfrage. Die Verbindung wird
nicht ausgeleitet, und eine Linter-Regel verbietet den direkten Import des
Treibers ausserhalb von lib/db/. Der Nachweis dazu ist
tests/integration/session-context.test.ts: ohne Kontext liefert die
Berechtigungsfunktion nie wahr, und eine Kennung leckt nicht über den
Verbindungspool in die nächste Anfrage.
Der Nachweis entsteht in der Datenbank, nicht im Anwendungscode. Jede ändernde SQL-Funktion schreibt ihren Protokolleintrag in derselben Transaktion wie die Änderung. Ein fehlgeschlagener Eintrag lässt den ganzen Vorgang scheitern.
Warum es keinen lib/audit/audit-log.ts gibt
Ein anwendungsseitiger Protokollschreiber wäre eine zweite Quelle neben
der bestehenden — und die einzige, die sich umgehen liesse, indem jemand die
Datenbankfunktion direkt aufruft. Die Aufgabenstellung nennt die Tabellen
audit_logs und planned_changes; im Schema heissen sie audit_log
(Einzahl) und pending_org_changes. Die vollständige Zuordnung steht im
Datenkatalog.
Der nächtliche Lauf
Die Route prüft Authorization über request.headers.get("authorization").
HTTP-Kopfzeilen sind unabhängig von Gross- und Kleinschreibung, und
Headers.get() behandelt sie entsprechend. Drei Tests decken das ab:
fehlende Kopfzeile, falscher Wert, nicht gesetztes CRON_SECRET. Die Antwort
ist jeweils 401, nicht 500.
Offen
- Neue Prüfung des heutigen Aufbaus — Entra ID,
app_users,withUser(), der nächtliche Lauf ohne Sonderrechte. - Inhaltsrichtlinie scharf schalten — sie läuft im Nur-Bericht-Modus, bis die Meldungen sauber sind.
- Zugangsdaten rotieren — siehe README.