test(auth): Gruppenisolation absichern, Doku zum Rechtemodell

AccessControlTest prueft die Trennung ueber die echte Kette
Auth -> AccessPolicy -> Repository, also genau so, wie die
Controller sie benutzen - nicht gegen eine nachgebaute Logik.
Abgedeckt sind unter anderem: ein Mitglied sieht nur die eigene
Gruppe, der direkte Aufruf einer fremden Kennung findet nichts,
ein Benutzer ohne Gruppe sieht gar nichts (die leere Liste darf
nicht wie "keine Einschraenkung" wirken), Viewer duerfen lesen
aber nicht schreiben, das Umhaengen einer Domain nimmt ihre
Kurzlinks mit, fremde Logos werden nicht ausgeliefert, ein
gesperrtes Konto verliert seine laufende Sitzung und ein
Passwortwechsel macht bestehende Sitzungen ungueltig.

MigrationUpgradeTest deckt zusaetzlich den Umstieg ab: das
Bestandskonto wird mit unveraendertem Hash uebernommen, die alten
settings-Schluessel verschwinden, Bestandsdomains landen in der
Standardgruppe - und eine frische Installation bekommt kein
erfundenes Konto.

README: neuer Abschnitt "Benutzer, Gruppen und Rechte" mit dem
Grundprinzip, den beiden Rollenebenen, der Einrichtung in drei
Schritten und dem Umstieg von der Einbenutzer-Version.
ADMIN_USER und ADMIN_PASSWORD_HASH sind als Notfallzugang neu
beschrieben, in README und .env.example.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-14 12:03:41 +02:00
co-authored by Claude Opus 5
parent 7f52613821
commit 85b9576b38
4 changed files with 609 additions and 30 deletions
+126 -13
View File
@@ -8,6 +8,10 @@ keine Node-Toolchain – nur PHP, eine Datenbank und ein Webserver.
Weiterleitungen `/{slug}` – Übersicht, Verwaltung, Statistiken und QR-Codes verlangen
Benutzername und Passwort.
**Mehrere Benutzer, in Gruppen organisiert.** Eine Domain gehört zu genau einer Gruppe,
jeder Kurzlink zu seiner Domain. Wer Mitglied einer Gruppe ist, sieht deren Kurzlinks –
als *Editor* darf er sie auch anlegen und ändern, als *Viewer* nur ansehen.
---
## Inhalt
@@ -18,6 +22,7 @@ Benutzername und Passwort.
- [Deployment B: Docker](#deployment-b-docker)
- [Konfigurationsreferenz](#konfigurationsreferenz)
- [Bedienung](#bedienung)
- [Benutzer, Gruppen und Rechte](#benutzer-gruppen-und-rechte)
- [Projektstruktur](#projektstruktur)
- [Sicherheit und Datenschutz](#sicherheit-und-datenschutz)
- [Tests](#tests)
@@ -60,9 +65,19 @@ Benutzername und Passwort.
- Download als **PNG** (Web) und **SVG** (vektorbasiert für den Druck)
- Live-Vorschau im Browser beim Anpassen
**Benutzer und Rechte**
- Beliebig viele Konten, verwaltet im Browser – kein Eingriff in Dateien nötig
- **Gruppen** bündeln Benutzer und Domains: Mitglieder sehen die Kurzlinks ihrer Gruppen,
sonst nichts – auch nicht über eine geratene Adresse
- Zwei Rollenebenen: global `Administrator` (Benutzer, Gruppen, Domains, sieht alles)
bzw. `Benutzer`, innerhalb einer Gruppe `Editor` (anlegen und ändern) bzw. `Viewer` (nur lesen)
- Jeder ändert sein Passwort selbst; eine Änderung beendet alle anderen Sitzungen
- Notfallzugang über die `.env`, falls das letzte Administratorkonto einmal aussperrt
**Sicherheit**
- Setup-Assistent beim ersten Start oder feste Zugangsdaten in der `.env`
- Session-Login mit bcrypt-Hash, CSRF-Token für alle Formulare, Rate-Limiting am Login
- Setup-Assistent beim ersten Start; Notfallzugang optional in der `.env`
- Session-Login mit bcrypt-Hash, CSRF-Token für alle Formulare
- Rate-Limiting am Login nach IP-Adresse **und** Benutzername
- Vorbereitete Statements überall, Output-Escaping, Security-Header inkl. CSP
---
@@ -315,14 +330,24 @@ Datei funktioniert in der Praxis ebenfalls, sofern gerade nicht geschrieben wird
Das Schema wird beim ersten Request automatisch angelegt (`migrations/`), ein
manueller Migrationsschritt entfällt.
### Admin-Zugang
### Notfallzugang
Die regulären Konten stehen in der Datenbank und werden unter **Benutzer** verwaltet;
das erste legt der Setup-Assistent an. Die beiden folgenden Schlüssel beschreiben
davon unabhängig einen **zusätzlichen** Zugang mit Administratorrechten:
| Schlüssel | Standard | Bedeutung |
|---|---|---|
| `ADMIN_USER` | `admin` | Benutzername |
| `ADMIN_PASSWORD_HASH` | *(leer)* | bcrypt-Hash. **Leer** → beim ersten Aufruf startet der Setup-Assistent und speichert die Zugangsdaten in der Datenbank. |
| `ADMIN_USER` | `admin` | Benutzername des Notfallzugangs |
| `ADMIN_PASSWORD_HASH` | *(leer)* | bcrypt-Hash dazu. **Leer** → es gibt keinen Notfallzugang. |
Hash selbst erzeugen:
Dieses Konto taucht in der Benutzerverwaltung nicht auf und lässt sich dort auch nicht
ändern – es existiert nur in der `.env`. Gedacht ist es für den Fall, dass das letzte
Administratorkonto gesperrt wurde oder sein Passwort verloren ging. Ein reguläres Konto
mit demselben Namen gewinnt bei der Anmeldung; die Benutzerverwaltung verhindert deshalb,
dass der Name doppelt vergeben wird.
Hash erzeugen:
```bash
php -r "echo password_hash('dein-passwort', PASSWORD_DEFAULT), PHP_EOL;"
@@ -330,8 +355,7 @@ php -r "echo password_hash('dein-passwort', PASSWORD_DEFAULT), PHP_EOL;"
docker compose exec app php -r "echo password_hash('dein-passwort', PASSWORD_DEFAULT), PHP_EOL;"
```
Ist `ADMIN_PASSWORD_HASH` gesetzt, gewinnt dieser Wert gegenüber der Datenbank –
das ist gleichzeitig der Weg, ein vergessenes Passwort zurückzusetzen.
Wird der Zugang nicht mehr gebraucht, beide Zeilen leeren oder entfernen.
### Sessions und Login-Schutz
@@ -372,8 +396,12 @@ das ist gleichzeitig der Weg, ein vergessenes Passwort zurückzusetzen.
### Anmelden
Beim ersten Aufruf erscheint der Setup-Assistent (sofern kein
`ADMIN_PASSWORD_HASH` gesetzt ist). Danach führt jeder Aufruf zur Anmeldemaske.
Beim ersten Aufruf erscheint der Setup-Assistent (sofern noch kein Konto existiert und
kein `ADMIN_PASSWORD_HASH` gesetzt ist). Das dort angelegte Konto ist Administrator.
Danach führt jeder Aufruf zur Anmeldemaske.
Das eigene Passwort ändert man über den **Benutzernamen oben rechts → Mein Konto**.
Dort steht auch, in welchen Gruppen man mit welcher Rolle Mitglied ist.
### Kurzlink anlegen
@@ -606,6 +634,89 @@ Zahlen `302` verwenden.
---
## Benutzer, Gruppen und Rechte
### Das Grundprinzip
Der Zugriff hängt an einer einzigen Kette:
```
Benutzer → Gruppe → Domain → Kurzlink
```
Ein Benutzer ist Mitglied in beliebig vielen **Gruppen**. Jede **Domain** gehört zu genau
einer Gruppe, jeder **Kurzlink** zu seiner Domain. Wer Mitglied einer Gruppe ist, sieht
damit alle Kurzlinks aller Domains dieser Gruppe – und sonst nichts.
Ein Kurzlink hat also keinen persönlichen Eigentümer. Das ist Absicht: verlässt jemand
das Unternehmen, bleiben die Links seiner Gruppe erreichbar, und gedruckte QR-Codes
funktionieren weiter. Das Löschen eines Kontos entfernt deshalb keine Kurzlinks.
### Die beiden Rollenebenen
**Global** (bei jedem Konto unter *Benutzer*):
| Rolle | Darf |
|---|---|
| `Administrator` | alles: Benutzer, Gruppen und Domains verwalten, sieht **sämtliche** Kurzlinks unabhängig von Gruppen |
| `Benutzer` | nur das, was die Gruppenmitgliedschaft hergibt |
**Innerhalb einer Gruppe** (je Mitgliedschaft):
| Rolle | Darf |
|---|---|
| `Editor` | Kurzlinks der Gruppe anlegen, ändern, deaktivieren, löschen und QR-Codes gestalten |
| `Viewer` | Kurzlinks ansehen, Statistik lesen, QR-Codes herunterladen – aber nichts speichern |
Ein Administrator braucht keine Mitgliedschaft, um Kurzlinks zu *sehen*. Damit er auch
selbst welche **anlegen** kann, muss er in der betreffenden Gruppe Editor sein – deshalb
trägt der Setup-Assistent das erste Konto gleich als Editor der Gruppe „Standard“ ein.
### Einrichten in drei Schritten
1. **Gruppen → Neue Gruppe** – z. B. „Marketing“.
2. **Domains → Domain bearbeiten** – Feld *Gruppe* auf „Marketing“ stellen. Alle Kurzlinks
dieser Domain gehören ab sofort der Gruppe. Wird eine Domain später umgehängt, wandern
ihre Kurzlinks mit.
3. **Benutzer → Neuer Benutzer** – Konto anlegen und in der Tabelle *Gruppen* die Rolle
je Gruppe setzen (`kein Mitglied` / `Editor` / `Viewer`).
Mitglieder lassen sich auch von der anderen Seite pflegen: **Gruppen → Bearbeiten** zeigt
alle Mitglieder samt Rolle und listet die Domains der Gruppe.
### Was passiert, wenn …
| Fall | Verhalten |
|---|---|
| Ein Benutzer ruft die Adresse eines fremden Kurzlinks direkt auf | **404** – die Existenz wird nicht verraten |
| Ein *Viewer* öffnet das Bearbeitungsformular | **403** mit Hinweis auf die fehlende Editor-Rolle |
| Ein Benutzer ohne Gruppe meldet sich an | Übersicht und Liste bleiben leer, mit erklärendem Hinweis |
| Ein Konto wird gesperrt | Laufende Sitzungen enden beim nächsten Aufruf sofort |
| Ein Passwort wird geändert | Alle anderen Sitzungen dieses Kontos werden ungültig |
| Das letzte aktive Administratorkonto soll herabgestuft, gesperrt oder gelöscht werden | Wird abgelehnt |
| Eine Gruppe mit zugeordneten Domains soll gelöscht werden | Wird abgelehnt – erst die Domains umhängen |
Auch hochgeladene **Logos** sind an die Gruppe gebunden: `/admin/uploads/<datei>` liefert
nur aus, was zu einem Kurzlink aus einer Gruppe des Aufrufers gehört. Der Dateiname allein
öffnet nichts.
### Umstieg von der Einbenutzer-Version
Beim ersten Aufruf nach dem Update läuft die Migration automatisch:
- Das bisherige Konto wird zu **Benutzer Nr. 1** mit der Rolle `Administrator`.
Benutzername und Passwort bleiben unverändert – der vorhandene Hash wird übernommen.
- Es entsteht die Gruppe **„Standard“**; alle vorhandenen Domains landen darin,
das übernommene Konto wird ihr Editor.
- Die alten Schlüssel `admin_user` und `admin_password_hash` werden aus der
`settings`-Tabelle entfernt, damit keine veralteten Zugangsdaten zurückbleiben.
Ein in der `.env` hinterlegter `ADMIN_PASSWORD_HASH` bleibt gültig, wird aber ab jetzt
als [Notfallzugang](#notfallzugang) behandelt: er steht neben den regulären Konten,
statt sie zu überstimmen.
---
## Projektstruktur
```
@@ -621,12 +732,14 @@ urlshort-qr-gen/
│ ├── Migrator.php ← führt migrations/ automatisch aus
│ ├── View.php ← minimale Template-Engine
│ ├── Http/ ← Request, Response, Router, BasePath
│ ├── Controller/ ← Setup, Auth, Dashboard, Link, Domain, Stats, Qr, Redirect
│ ├── Controller/ ← Setup, Auth, Dashboard, Link, Domain, Stats, Qr,
│ │ User, Group, Profile, Redirect
│ ├── Repository/ ← Datenzugriff (vorbereitete Statements)
│ ├── Security/ ← Session, Auth, CSRF, Rate-Limiting
│ ├── Security/ ← Session, Auth, AccessPolicy, Role, CSRF, Rate-Limiting
│ ├── Service/ ← DomainChecker (DNS- und Zuordnungsprüfung)
│ ├── Qr/ ← QrOptions, QrRenderer, LogoStore
│ └── Support/ ← Slug, UrlValidator, Punycode, ShortDomain, UserAgentParser, Color
│ └── Support/ ← Slug, UrlValidator, Punycode, ShortDomain,
│ UserAgentParser, Color, Password, Username
├── templates/ ← serverseitig gerenderte Seiten
├── migrations/ ← Schema je Treiber
├── data/ ← SQLite-Datei + hochgeladene Logos (beschreibbar!)