„Was ist eigentlich mit meinem Notebook passiert?“ musste bisher ein Techniker nachsehen. Ein Mitarbeiter kann die Arbeiten an **seinen** Geraeten jetzt selbst ansehen — mit der fuenften Rolle MITARBEITER, zugeordnet beim Anmelden ueber den UPN. Was er sieht, sind Titel, Zeitraum und Status. Keine Schritte, keine Notizen, keine Kommentare, keine Anhaenge: in einem Lauf stehen Arbeitsnotizen ueber Menschen und Befehle mit Platzhaltern fuer Zugangsdaten. Eine Selbstauskunft ist keine Akteneinsicht, und das haelt nicht die Seite fest, sondern das Lesemodell — eine Seite, die filtert, ist eine Seite, die es vergessen kann. `selbstauskunft` ist ein **Ersatz** fuer `lesen` und kein Zusatz; auch ein ADMIN hat es nicht. Ein zweiter Weg zu denselben Daten waere eine zweite Stelle, an der jemand die Einschraenkung auf die eigene Person vergisst. Die Datenbank verlangt das sogar: ein CHECK haelt fest, dass Rolle und Zuordnung zusammengehoeren, in beide Richtungen. Ein mehrdeutiger UPN wird abgewiesen und protokolliert. Zwei Kunden koennen dieselbe Person fuehren; ein findFirst entschiede nach Sortierung darueber, wessen Geraete jemand sieht. **Verhaltensaenderung:** wer sich anmeldet und in keiner Rollengruppe steht, bekommt nicht mehr automatisch LESER — also nicht mehr Lesezugriff auf alle Laeufe des Hauses. Das greift nur mit konfigurierten OIDC_GROUP_*; ohne sie bleibt alles wie vorher, denn ohne abgebildete Gruppen laesst sich Techniker nicht von Mitarbeiter unterscheiden. Nebenbei behoben: `staerksteRolle` benutzte LESER als Startwert und damit als Untergrenze — mit einer Rolle darunter lieferte sie mehr Rechte als uebergeben. Gefunden von einem Test, der seit #007 unveraendert dasteht. Closes #55 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
163 lines
6.3 KiB
TypeScript
163 lines
6.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
|
|
|
/**
|
|
* Wächter über die Datenbankobjekte, die Prisma nicht kennt (#009).
|
|
*
|
|
* Zusammengesetzte Fremdschlüssel, Trigramm-Indizes und Trigger lassen sich im
|
|
* Prisma-Schema nicht ausdrücken. Deshalb erzeugt `prisma migrate dev` bei
|
|
* **jeder** künftigen Migration Löschbefehle für sie — sie sind aus Prismas
|
|
* Sicht Abweichungen von der Vorgabe. Wer die Löschbefehle einmal übersieht,
|
|
* entfernt stillschweigend die Mandantengrenze, die Archivsuche oder den Schutz
|
|
* der Unveränderlichkeit.
|
|
*
|
|
* Dieser Test macht das laut. Er prüft nicht, dass die Regeln *wirken* — das tun
|
|
* `tests/datenmodell.test.ts` und `tests/vorlagen.test.ts` —, sondern nur, dass
|
|
* die Objekte überhaupt noch da sind.
|
|
*/
|
|
|
|
async function namen(abfrage: string): Promise<string[]> {
|
|
const zeilen = await testPrisma.$queryRawUnsafe<{ name: string }[]>(abfrage);
|
|
return zeilen.map((zeile) => zeile.name).sort();
|
|
}
|
|
|
|
describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
|
|
it("hat die zusammengesetzten Fremdschlüssel der Mandantengrenze", async () => {
|
|
// Ohne sie kann ein Lauf ein Gerät eines fremden Kunden ziehen — und ein
|
|
// Anhang an einem Schritt hängen, der zu einem anderen Lauf gehört.
|
|
expect(
|
|
await namen(
|
|
`SELECT conname AS name FROM pg_constraint
|
|
WHERE conname LIKE '%gleicher_kunde' OR conname LIKE '%gleicher_lauf'`,
|
|
),
|
|
).toEqual([
|
|
"Anhang_ergebnis_gleicher_lauf",
|
|
"Anhang_lauf_gleicher_kunde",
|
|
"Geraet_besitzer_gleicher_kunde",
|
|
"Lauf_abteilung_gleicher_kunde",
|
|
"Lauf_geraet_gleicher_kunde",
|
|
"Lauf_mitarbeiter_gleicher_kunde",
|
|
"Mitarbeiter_vorgesetzter_gleicher_kunde",
|
|
]);
|
|
});
|
|
|
|
it("hat die Trigramm-Indizes für die Archivsuche", async () => {
|
|
// Ohne sie wird jede Archivsuche zum vollständigen Durchlauf der Tabelle.
|
|
expect(
|
|
await namen(
|
|
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%gin_trgm_ops%'`,
|
|
),
|
|
).toEqual([
|
|
"Kommentar_rumpf_trgm_idx",
|
|
"Lauf_titel_trgm_idx",
|
|
"Schrittergebnis_notiz_trgm_idx",
|
|
"Schrittergebnis_wert_trgm_idx",
|
|
]);
|
|
});
|
|
|
|
it("hat die Trigger, die eine veröffentlichte Version schützen", async () => {
|
|
expect(
|
|
await namen(
|
|
`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal AND tgname NOT LIKE '%_melden'`,
|
|
),
|
|
).toEqual([
|
|
"BausteinSchritt_unveraenderlich",
|
|
"BausteinVersion_status_einbahn",
|
|
/*
|
|
* Zieht die Rolle mit, wenn eine Person geloescht wird (#035). Ohne ihn
|
|
* scheiterte das `ON DELETE SET NULL` des Fremdschluessels am CHECK, und
|
|
* das Loeschen eines Mitarbeiters waere unmoeglich statt folgerichtig.
|
|
*/
|
|
"Mitarbeiter_konto_mitziehen",
|
|
"VorlagenAbschnitt_baustein_veroeffentlicht",
|
|
"VorlagenAbschnitt_unveraenderlich",
|
|
"VorlagenSchritt_unveraenderlich",
|
|
"VorlagenVariable_unveraenderlich",
|
|
"VorlagenVersion_status_einbahn",
|
|
]);
|
|
});
|
|
|
|
it("hat die Trigger, die eine Änderung am Lauf melden", async () => {
|
|
/*
|
|
* Ohne sie bleibt die Live-Aktualisierung still, und zwar **lautlos**: die
|
|
* Seite sieht aus wie immer, nur sieht niemand mehr die Arbeit des anderen.
|
|
* Welche Tabellen dabei sind, haengt an dem, was `laufZustand` liefert —
|
|
* fehlte `Kommentar` oder `Anhang`, waere der Strom gegenueber dem alten
|
|
* Polling ein Rueckschritt (#033).
|
|
*/
|
|
expect(
|
|
await namen(`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal AND tgname LIKE '%_melden'`),
|
|
).toEqual([
|
|
"Anhang_geaendert_melden",
|
|
"Kommentar_geaendert_melden",
|
|
"Lauf_geaendert_melden",
|
|
"Schrittergebnis_geaendert_melden",
|
|
]);
|
|
});
|
|
|
|
it("hat die partiellen Eindeutigkeitsindizes", async () => {
|
|
expect(
|
|
await namen(
|
|
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%WHERE%'`,
|
|
),
|
|
).toEqual([
|
|
"BausteinVersion_ein_entwurf",
|
|
"Kunde_eigene_organisation_einmalig",
|
|
"Vorlage_seedSchluessel_einmalig",
|
|
"VorlagenVersion_ein_entwurf",
|
|
/*
|
|
* Ohne diesen bekaeme der Zustaendige fuer einen Lauf, der ueber das
|
|
* Wochenende ueberfaellig steht, 2880 Mails — und zwar ohne dass ein
|
|
* einziger Test rot waere, denn jede davon ist fuer sich korrekt (#034).
|
|
*/
|
|
"Zustellung_ueberfaellig_einmal",
|
|
]);
|
|
});
|
|
|
|
it("hat alle Prüfbedingungen", async () => {
|
|
const vorhanden = await namen(
|
|
`SELECT conname AS name FROM pg_constraint WHERE contype='c' AND connamespace='public'::regnamespace`,
|
|
);
|
|
// Namentlich aufgezählt, damit auch das Verschwinden einer einzelnen auffällt.
|
|
expect(vorhanden).toEqual([
|
|
"Anhang_groesse_positiv",
|
|
"Anhang_pruefsumme_form",
|
|
/*
|
|
* Die Rolle MITARBEITER und ihre Zuordnung gehoeren zusammen, in beide
|
|
* Richtungen (#035). Ohne das waere ein MITARBEITER ohne Person moeglich —
|
|
* ein Recht, das sich auf niemanden bezieht, und eine Einschraenkung, die
|
|
* je nach Formulierung „nichts“ oder „alles“ bedeutet.
|
|
*/
|
|
"Benutzer_mitarbeiter_nur_bei_rolle",
|
|
"Einstellung_einzelne_zeile",
|
|
"Einstellung_smtp_passwort_verschluesselt",
|
|
"Einstellung_smtp_port_bereich",
|
|
"Einstellung_webhook_geheimnis_verschluesselt",
|
|
"Geraet_registriert_braucht_namen",
|
|
"Geraet_tagcode_form",
|
|
"Kunde_kuerzel_form",
|
|
"Lauf_abgeschlossen_mit_zeitpunkt",
|
|
"Lauf_archiviert_mit_zeitpunkt",
|
|
"Lauf_begonnen_mit_zeitpunkt",
|
|
"Lauf_mitarbeiter_eindeutig",
|
|
"Mitarbeiter_vorgesetzter_nicht_selbst",
|
|
"Schrittergebnis_begruendung_noetig",
|
|
"TagCodeVergabe_form",
|
|
"Verzeichnisquelle_bindpasswort_verschluesselt",
|
|
"VorlagenVersion_veroeffentlicht_mit_zeitpunkt",
|
|
"Zustellung_bezug_passt_zum_anlass",
|
|
"Zustellung_empfaenger_nicht_leer",
|
|
"Zustellung_gesendet_mit_zeitpunkt",
|
|
"Zustellung_versuche_nicht_negativ",
|
|
"Zustellung_webhook_ohne_benutzer",
|
|
]);
|
|
});
|
|
|
|
it("hat die Erweiterung für die Trigramm-Suche", async () => {
|
|
const zeilen = await testPrisma.$queryRaw<{ extname: string }[]>`
|
|
SELECT extname FROM pg_extension WHERE extname = 'pg_trgm'
|
|
`;
|
|
expect(zeilen).toHaveLength(1);
|
|
});
|
|
});
|