Drei Rollen: Administrator, Techniker, Kunde. Jede der 28 Aktionen nennt das Recht, das sie verlangt; die Rechte stehen an einer Stelle. Kundenkonten haengen an genau einem Kunden und sehen nur dessen Hosts und Rechnungen - ohne Einkaufspreise und Marge. Die Einschraenkung sitzt in den Abfragen selbst, damit keine Seite vergessen kann, sie weiterzugeben; sechs Tests pruefen das. Dabei gefunden: Neu ueber OIDC angelegte Konten haetten die Vorgabe des Datenmodells geerbt - ADMIN. Jetzt gibt es dafuer eine Einstellung mit der Vorgabe Techniker. Passkeys (#028) als gleichwertiger, passwortloser Anmeldeweg neben Passwort und OIDC. Mehrere je Konto, Aufgabe in einem kurzlebigen Cookie, rpID aus der oeffentlichen Adresse. Ohne HTTPS sagt die Seite es, statt still zu scheitern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@@ -18,6 +18,7 @@ auch mit Hosts hinter NAT.
|
||||
8. [Benachrichtigungen einrichten](#8-benachrichtigungen-einrichten)
|
||||
9. [Exporte](#9-exporte)
|
||||
9a. [Einstellungen](#9a-einstellungen)
|
||||
9b. [Benutzer, Rollen und Passkeys](#9b-benutzer-rollen-und-passkeys)
|
||||
10. [Sicherung und Aktualisierung](#10-sicherung-und-aktualisierung)
|
||||
11. [Fehlersuche](#11-fehlersuche)
|
||||
|
||||
@@ -341,6 +342,46 @@ Zeitzone gilt für PDF, CSV und Benachrichtigungen.
|
||||
|
||||
---
|
||||
|
||||
## 9b. Benutzer, Rollen und Passkeys
|
||||
|
||||
Unter *Einstellungen → Benutzer* legen Administratoren Konten an und vergeben eine von drei Rollen:
|
||||
|
||||

|
||||
|
||||
| Rolle | Darf |
|
||||
|---|---|
|
||||
| **Administrator** | alles, einschließlich Benutzer und Systemeinstellungen |
|
||||
| **Techniker** | alles Fachliche: Hosts, Kunden, Rechnungen, Tokens, Warnregeln – aber keine Benutzer |
|
||||
| **Kunde** | nur die Hosts und Rechnungen des eigenen Kunden, **ohne Einkaufspreise und Marge**; darf Notizen schreiben und Warnungen quittieren |
|
||||
|
||||
Ein Kundenkonto ist an genau einen Kunden gebunden – das Feld erscheint, sobald die Rolle „Kunde“
|
||||
gewählt ist. Die Einschränkung sitzt in den Abfragen selbst, nicht in der Anzeige: Was ein Kundenkonto
|
||||
nicht sehen darf, steht auch nicht im ausgelieferten HTML.
|
||||
|
||||
Ein Passwort ist beim Anlegen freiwillig. Ohne Passwort entsteht ein Konto, das sich nur über OIDC oder
|
||||
einen Passkey anmeldet. **Ein neues Passwort beendet alle laufenden Sitzungen dieses Kontos**, ebenso
|
||||
das Sperren.
|
||||
|
||||
### Passkeys
|
||||
|
||||
Ein Passkey ersetzt das Passwort: Der Browser bestätigt die Anmeldung mit Fingerabdruck, Gesicht, PIN
|
||||
oder einem Sicherheitsschlüssel. Jeder Benutzer verwaltet seine eigenen unter *Meine Passkeys*:
|
||||
|
||||

|
||||
|
||||
Zwei Dinge sind wichtig:
|
||||
|
||||
1. **Passkeys brauchen HTTPS.** Nur `localhost` ist ausgenommen. Wird das Dashboard über `http://`
|
||||
im LAN aufgerufen, weist die Seite darauf hin und der Knopf bleibt gesperrt.
|
||||
2. **Der Passkey hängt an der Adresse.** Wer später von `http://server:3000` auf
|
||||
`https://proxledger.firma.de` wechselt, muss neu registrieren. Deshalb zuerst den Reverse Proxy
|
||||
einrichten, dann Passkeys anlegen.
|
||||
|
||||
Passwort, OIDC und Passkey stehen gleichberechtigt nebeneinander – der lokale Zugang bleibt als
|
||||
Rückweg immer nutzbar.
|
||||
|
||||
---
|
||||
|
||||
## 10. Sicherung und Aktualisierung
|
||||
|
||||
**Sichern:**
|
||||
|
||||
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 253 KiB |
@@ -0,0 +1,36 @@
|
||||
# #027 Rollen und Benutzerverwaltung
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.18.0
|
||||
**Labels:** security, backend, ui
|
||||
**Geschätzt:** 6 h **Tatsächlich:** 1 h 20 min
|
||||
|
||||
## Ziel
|
||||
Bisher durfte jeder Angemeldete alles – in `docs/sicherheit.md` als offener Punkt vermerkt. Es soll
|
||||
drei Rollen geben: Administrator, Techniker und Kunde. Kundenkonten sehen nur die eigenen Daten.
|
||||
|
||||
## Aufgaben
|
||||
- [x] `Rolle` im Datenmodell, Kundenbindung am Benutzer
|
||||
- [x] Rechte an **einer** Stelle (`src/server/auth/rollen.ts`), nicht verteilt über Seiten
|
||||
- [x] `sitzungErforderlich(recht)` – jede der 28 Aktionen nennt ihr Recht
|
||||
- [x] Sicht einschränken: Hosts, Kunden, Rechnungen, Kennzahlen, Suche, Jahresbahn, Dashboard
|
||||
- [x] Einkaufspreise und Marge für Kundenkonten entfernen – in der Abfrage, nicht in der Anzeige
|
||||
- [x] Schreibrechte für Kunden: Notizen am eigenen Host, Warnungen quittieren
|
||||
- [x] Benutzerverwaltung unter *Einstellungen → Benutzer*
|
||||
- [x] Menü nach Rolle
|
||||
- [x] Tests
|
||||
|
||||
## Entscheidungen
|
||||
- **Rolle und Kundenbindung kommen aus der Datenbank, nicht aus dem Token.** Eine geänderte Rolle
|
||||
wirkt damit sofort statt erst nach Ablauf der Sitzung.
|
||||
- **Die Abfragen holen sich die Einschränkung selbst** (`sichtbarerKunde()`), statt sie von der Seite
|
||||
gereicht zu bekommen. Eine Seite kann so nicht vergessen, sie weiterzugeben – der häufigste Weg,
|
||||
wie solche Sichten lecken.
|
||||
- **Ein Kundenkonto ohne Kundenbindung sieht nichts** statt alles. Der Rückfall zeigt bewusst ins
|
||||
Leere, nicht auf den vollen Bestand.
|
||||
- **Ausgeblendete Menüpunkte sind keine Zugriffskontrolle.** Sie sind Bequemlichkeit; geprüft wird in
|
||||
Seite und Aktion.
|
||||
|
||||
## Beim Bauen aufgefallen
|
||||
Neu angelegte OIDC-Konten hätten die Vorgabe des Datenmodells geerbt – **ADMIN**. Wer sich beim
|
||||
Identitätsanbieter anmelden darf, wäre damit nebenbei Administrator geworden. Jetzt gibt es dafür eine
|
||||
eigene Einstellung mit der Vorgabe *Techniker*, samt Test.
|
||||
@@ -0,0 +1,35 @@
|
||||
# #028 Anmeldung mit Passkeys
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.18.0
|
||||
**Labels:** security, backend, ui
|
||||
**Geschätzt:** 4 h **Tatsächlich:** 0 h 45 min
|
||||
|
||||
## Ziel
|
||||
Passkeys (WebAuthn) als **gleichwertiger** Anmeldeweg neben Passwort und OIDC – passwortlos, mit
|
||||
Fingerabdruck, Gesicht, PIN oder Sicherheitsschlüssel.
|
||||
|
||||
## Aufgaben
|
||||
- [x] `@simplewebauthn/server` und `@simplewebauthn/browser`
|
||||
- [x] Datenmodell `Passkey` (mehrere je Konto)
|
||||
- [x] Registrierung und Anmeldung als Endpunkte, Aufgabe („challenge“) in einem kurzlebigen Cookie
|
||||
- [x] Seite *Meine Passkeys* mit Anlegen und Entfernen
|
||||
- [x] Knopf „Mit Passkey anmelden“ auf der Anmeldeseite
|
||||
- [x] Hinweis, wenn die Adresse kein HTTPS hat
|
||||
|
||||
## Entscheidungen
|
||||
- **Keine Liste erlaubter Schlüssel bei der Anmeldung.** Der Browser bietet selbst an, was zu dieser
|
||||
Seite passt. Sonst müsste die Seite vorher verraten, welche Passkeys zu einem Konto gehören – das
|
||||
wäre ein Weg, Konten auszuspähen.
|
||||
- **Fehlermeldung bei der Anmeldung bewusst unscharf** („Anmeldung mit diesem Passkey nicht
|
||||
möglich“). Ob es den Schlüssel gibt, geht einen Unbekannten nichts an.
|
||||
- **Die Aufgabe liegt in einem `SameSite=strict`-Cookie mit fünf Minuten Gültigkeit**, nicht in der
|
||||
Datenbank. Sie gilt nur für diesen Browser und diesen Versuch.
|
||||
- **`rpID` und `origin` kommen aus der öffentlichen Adresse** (Einstellung, sonst `APP_URL`, sonst
|
||||
Proxy-Kopfzeilen). Ein Passkey ist an die Domäne gebunden; steht dort die falsche Adresse, lehnt
|
||||
der Browser wortlos ab.
|
||||
- Passwort und OIDC bleiben bestehen. Ein Passkey, der im kaputten Telefon liegt, darf niemanden
|
||||
aussperren.
|
||||
|
||||
## Grenze
|
||||
Ein Passkey gilt nur für die Adresse, unter der er angelegt wurde. Beim Umzug von `http://server:3000`
|
||||
auf `https://proxledger.firma.de` müssen alle Passkeys neu registriert werden – im Handbuch vermerkt.
|
||||
@@ -30,4 +30,6 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
|
||||
| [#024](024-owasp-haertung.md) | Härtung nach OWASP | erledigt | v0.15.0 | 4 h | 0 h 45 min |
|
||||
| [#025](025-einstellungen.md) | Einstellungen im Dashboard | erledigt | v0.16.0 | 5 h | 1 h 10 min |
|
||||
| [#026](026-auslieferung.md) | Auslieferung finalisieren | erledigt | v0.17.0 | 3 h | 0 h 40 min |
|
||||
| | **Summe** | | | **133 h** | **12 h 18 min** |
|
||||
| [#027](027-rollen-benutzerverwaltung.md) | Rollen und Benutzerverwaltung | erledigt | v0.18.0 | 6 h | 1 h 20 min |
|
||||
| [#028](028-passkeys.md) | Anmeldung mit Passkeys | erledigt | v0.18.0 | 4 h | 0 h 45 min |
|
||||
| | **Summe** | | | **143 h** | **14 h 23 min** |
|
||||
|
||||
@@ -15,8 +15,13 @@ Die Gliederung folgt den OWASP Top 10 (2021).
|
||||
- Die Anmeldung prüft den `Origin`, damit keine fremde Seite jemanden unbemerkt in ein untergeschobenes
|
||||
Konto anmeldet (Login-CSRF). Für Server Actions erledigt das Next.js selbst.
|
||||
|
||||
**Nicht vorhanden:** Rollen. Wer angemeldet ist, darf alles. Für ein Werkzeug, das ein Systemhaus intern
|
||||
betreibt, ist das vertretbar – für mehrere Mandanten wäre es das nicht.
|
||||
**Rollen** (#027): Administrator, Techniker und Kunde. Jede Aktion nennt das Recht, das sie verlangt;
|
||||
die Rechte stehen an einer Stelle (`src/server/auth/rollen.ts`). Kundenkonten sind an einen Kunden
|
||||
gebunden, und die Einschränkung sitzt in den Abfragen – nicht in der Anzeige. Einkaufspreise und Marge
|
||||
werden für sie schon beim Abfragen entfernt, stehen also nicht im ausgelieferten HTML.
|
||||
|
||||
**Weiterhin nicht vorhanden:** Mandantenfähigkeit im eigentlichen Sinn. Mehrere Systemhäuser in einer
|
||||
Installation wären ein Umbau, kein Anbau.
|
||||
|
||||
## A02 – Kryptografische Fehler
|
||||
|
||||
@@ -70,8 +75,13 @@ gespeicherten Zugangsdaten unlesbar. Er gehört in die Sicherung.
|
||||
- Anmeldung wahlweise per Passwort oder OIDC (authentik, mit PKCE und `state`).
|
||||
- Jede Anmeldung – erfolgreich oder nicht – landet im Audit-Log.
|
||||
|
||||
**Nicht vorhanden:** Zwei-Faktor-Anmeldung. Wer sie braucht, stellt OIDC davor und schaltet die
|
||||
Passwortanmeldung ab.
|
||||
- **Passkeys** (WebAuthn, #028) als dritter, gleichwertiger Weg. Passwortlos, an die Domäne gebunden,
|
||||
mehrere je Konto. Setzt HTTPS voraus (außer auf `localhost`).
|
||||
- Automatisch angelegte OIDC-Konten bekommen die eingestellte Rolle – Vorgabe *Techniker*, nicht
|
||||
Administrator.
|
||||
|
||||
**Nicht vorhanden:** klassische Zwei-Faktor-Anmeldung mit Einmalkennwort. Ein Passkey ersetzt beides
|
||||
in einem Schritt; wer einen zweiten Faktor zum Passwort erzwingen will, stellt OIDC davor.
|
||||
|
||||
## A08 – Datenintegrität
|
||||
|
||||
|
||||