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:
@@ -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!)
|
||||
|
||||
Reference in New Issue
Block a user