Files
bf-infra/adguard/ha-self-service.md
Christoph Emsenhuber ae87e9f511 TP23 Stufe 5 Redesign: pro-Person Self-Service (agh-exception.py + Bau-Spec)
Stufe 5 reopened — DAU-Requirements (pro Person, "zuletzt geblockt -> 1 Klick",
temp+dauerhaft umschaltbar, scoped HA-Dashboard) nicht erfuellt → Stufe 6 gated.
agh-exception.py: $client-scoped allow/unallow, list-exceptions, recent-blocked
(selftest OK, reine Logik netzunabhaengig getestet). HA-Bau-Spec + Personen-
Client-Modell dokumentiert. Bau/Abnahme on-network offen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-12 14:32:13 +02:00

99 lines
7.0 KiB
Markdown

# TP23 Stufe 5 (Redesign 2026-09-12) — DAU-tauglicher, personenbezogener DNS-Self-Service
> 🔴 **Status: Bau-Spec, noch nicht deployt.** Die alte Fassung (single-domain, temp-only,
> HA-Neustart bei neuer Domain) war „funktional ein kastriertes Pi-hole" (User-Befund 2026-09-12)
> und ist ersetzt. Bau + Abnahme laufen **on-network** (HAOS `10.40.20.105`, AGH `.36/.37`) — beim
> Anlegen den **home-assistant-best-practices-Skill** konsultieren und via **ha-mcp** in `.storage`
> anlegen (native Helper, `entity_id` statt `device_id`). Diese Datei ist die Repro-/SSoT-Quelle.
## Ziel (User-Requirements 2026-09-12)
Ein **einfaches, klar verständliches** End-User-UI zum Entsperren:
- **alles was den Nutzer betrifft, nichts** von anderen / Admin / Power-User;
- Policy nur sichtbar, wo sie für den einfachen Nutzer Relevanz hat.
| # | Requirement | Umsetzung |
|---|---|---|
| 1 | **Pro Person** (Login), Sicht über alle Geräte | HA-User = Person; Person → AGH-Client (Name), dessen `ids` = ihre Geräte-IPs |
| 2 | **„zuletzt geblockt" → 1 Klick** | `recent-blocked --client <person>` aus dem AGH-Query-Log, auf die Geräte der Person gefiltert |
| 3 | **temporär UND dauerhaft**, Modus umschaltbar | Dauer-Select inkl. „dauerhaft"; `input_boolean` self-permanent ⇄ Anfrage. **Start: self-permanent** |
| 4 | **HA-Dashboard, scoped** pro Person | ein View je Person, nur für ihre HA-User-ID sichtbar |
## Identitätsmodell — AGH-Client je Person ist die SSoT
- Pro Person **ein AGH-Client** (`/control/clients`), `name` = Personenname, `ids` = **alle** Geräte-IPs
der Person (die per DHCP faktisch statisch sind). Damit ist Person→Gerät **an einem Ort** gepflegt
(kein zweites Mapping-File), und Ausnahmen sind stabil bei Gerätewechsel.
- Ausnahme-Regel = `@@||domain^$client='Personenname'` (statt globalem `@@||domain^`).
🔴 **Vor Scharfschalten on-network verifizieren, dass AGH `$client=` auf `@@`-Regeln wirkt**
(s. `agh-exception.py` Kopf). Fällt der Test negativ aus: Fallback = `$client=<ip>|<ip>` mit den
Geräte-IPs statt dem Namen (Code kann beides, `scoped_rule()`).
- Die bestehenden **VLAN-CIDR-Gruppen** bleiben die Policy-Grenze (Filter an/aus je VLAN);
Personen-Clients sind spezifischer und tragen nur die *Ausnahmen*.
## CLI (deployt nach `/config/agh-exception.py`, wirkt auf `.36`+`.37`)
```
allow-domain <domain> --client <person> # Ausnahme setzen (temp: HA revertiert; perm: kein Timer)
unallow-domain <domain> --client <person> # dieselbe Ausnahme entfernen
list-exceptions --client <person> --json # „meine Ausnahmen" (für Übersicht + Entfernen)
recent-blocked --client <person> --json # „zuletzt geblockt" (nur Geräte der Person)
```
`--client` nimmt einen AGH-Client-Namen (→ IPs via `/control/clients`) ODER eine Komma-IP/CIDR-Liste.
## `configuration.yaml` — `shell_command` (einziger YAML-Teil, Backup `.bak-tp23-*`)
Domain **und** Client sind Parameter → **kein HA-Neustart mehr** bei neuer Domain (das war der alte
DAU-Killer: pro Domain ein eigener `shell_command`).
```yaml
shell_command:
agh_allow: "python3 /config/agh-exception.py allow-domain '{{ domain }}' --client '{{ client }}'"
agh_unallow: "python3 /config/agh-exception.py unallow-domain '{{ domain }}' --client '{{ client }}'"
```
## Helfer / Entities (via ha-mcp in .storage)
Pro Person (Beispiel `christoph`) — bei mehreren Personen je einmal, Suffix = Personen-Slug:
| Typ | entity_id | Zweck |
|---|---|---|
| command_line sensor | `sensor.dns_recent_blocked_<slug>` | `recent-blocked --client <person> --json` → `json_attributes: items` (Liste {domain,count,last}); Scan-Intervall ~60 s |
| command_line sensor | `sensor.dns_exceptions_<slug>` | `list-exceptions --client <person> --json` → aktive Ausnahmen der Person |
| input_select | `input_select.dns_dauer_<slug>` | 1 Stunde / 4 Stunden / **dauerhaft** |
| timer | `timer.dns_ausnahme_<slug>` | JIT-Countdown je temporärer Freigabe (restore=false) |
| input_boolean | `input_boolean.dns_permanent_ohne_anfrage` | **global**: an = „dauerhaft" wirkt sofort (Start); aus = erzeugt Admin-Genehmigung |
| script | `script.dns_freigeben_<slug>` | nimmt `domain` als Feld → `agh_allow`; bei „dauerhaft"+Switch-aus → `todo`/Notification statt Freigabe; sonst Timer starten |
| automation | `automation.dns_ablauf_<slug>` | `timer.finished`/`.cancelled` → `agh_unallow`, Aufräumen |
## Dashboard — ein scoped View je Person
- Eigener storage-Dashboard-View, **nur für die HA-User-ID der Person sichtbar** (`visible:` mit
der user-id; `require_admin:false`). Kleiner Haushalt → ein View je Person ist praktikabel und
umgeht das „Template kennt den aktuellen Betrachter nicht"-Problem.
- **Karte 1 „Zuletzt geblockt (antippen zum Freigeben)":** rendert `sensor.dns_recent_blocked_<slug>`
Attribut `items`; jede Zeile hat eine `tap_action` → `script.dns_freigeben_<slug>` mit `domain`
der Zeile + aktueller `input_select`-Dauer. (Dynamische Per-Zeilen-tap_action braucht i.d.R.
`auto-entities` + `template`/`button-card` **oder** ein kleines Custom-List-Template — **beim Bau
on-network entscheiden**, das ist der Teil, der Live-Verifikation braucht.)
- **Karte 2 „Meine Ausnahmen":** `sensor.dns_exceptions_<slug>` als Liste, je Eintrag Entfernen
(`agh_unallow`).
- **Karte 3 „Dauer":** `input_select.dns_dauer_<slug>` (inkl. „dauerhaft").
- **Kein** globaler Status, keine fremden Clients, keine Policy-Interna. Nur wenn relevant ein
kurzer Hinweis („durch den Familien-Filter blockiert").
## Genehmigungs-Variante (Modus-Switch aus)
`input_boolean.dns_permanent_ohne_anfrage = off` → „dauerhaft"-Klick legt statt der Freigabe einen
Eintrag in einer **`todo`-Liste** „DNS-Freigaben zu prüfen" an (+ Notification an Christoph). Beim
Abhaken führt eine Admin-Automation `agh_allow` ohne Timer aus. Beide Varianten sind gebaut; der
Switch entscheidet zur Laufzeit.
## Alt-Reste, die beim Bau wegfallen/fixen
- Single-domain-Flow (`input_text.dns_ausnahme_domain/_aktiv`, `input_button…_starten`,
`automation…_starten/_ablauf`) und der `agh_allow_domain`/`_unallow_domain`-**pro-Domain**-Zwang.
- Statuskachel-Sensor `binary_sensor.adguard_bf1_status` hing am toten `.16` → auf `.36`/`.37` neu.
## Abnahme (on-network, = neue echte Stufe-6-Vorbedingung)
Mit einem **echten Nicht-Admin-Login**: geblockte Seite → erscheint in *ihrer* Fläche → 1 Klick
temporär (revertiert nach Ablauf) **und** 1 Klick dauerhaft (bleibt) → wirkt **nur** für diese
Person (Gegentest: anderer Client sieht die Domain weiter geblockt). `.36 == .37`
(`agh-sync.py check`). Modus-Switch beide Wege einmal durchgespielt.
## Verweise
- CLI: `adguard/agh-exception.py` (Subcommands + `selftest`). Deploy-Weg unverändert (base64→scp,
s. unten). Creds `/config/agh-exception.secrets` (chmod 600).
- Redeploy: `base64 < adguard/agh-exception.py | ssh admin@10.40.20.105 "base64 -d | sudo tee /config/agh-exception.py >/dev/null"`.
- Personen-Client-Modell: `adguard/README.md`. Kontext/Reihenfolge: TP23 in `docs/teilprojekte.md`.