pillarz 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pillarz-0.1.0/CHANGELOG.md +536 -0
- pillarz-0.1.0/LICENSE +661 -0
- pillarz-0.1.0/MANIFEST.in +7 -0
- pillarz-0.1.0/PKG-INFO +71 -0
- pillarz-0.1.0/README.md +36 -0
- pillarz-0.1.0/env.example +93 -0
- pillarz-0.1.0/pillarz/__init__.py +618 -0
- pillarz-0.1.0/pillarz/__main__.py +176 -0
- pillarz-0.1.0/pillarz/_api/__init__.py +15 -0
- pillarz-0.1.0/pillarz/_api/account.py +66 -0
- pillarz-0.1.0/pillarz/_api/admin.py +55 -0
- pillarz-0.1.0/pillarz/_api/admin_links.py +24 -0
- pillarz-0.1.0/pillarz/_api/admin_log.py +41 -0
- pillarz-0.1.0/pillarz/_api/admin_sessions.py +43 -0
- pillarz-0.1.0/pillarz/_api/admin_settings.py +68 -0
- pillarz-0.1.0/pillarz/_api/admin_themes.py +96 -0
- pillarz-0.1.0/pillarz/_api/admin_users.py +357 -0
- pillarz-0.1.0/pillarz/_api/common.py +37 -0
- pillarz-0.1.0/pillarz/_api/me.py +203 -0
- pillarz-0.1.0/pillarz/_api/tokens.py +98 -0
- pillarz-0.1.0/pillarz/_auth/__init__.py +4 -0
- pillarz-0.1.0/pillarz/_auth/accounts.py +336 -0
- pillarz-0.1.0/pillarz/_auth/devices.py +51 -0
- pillarz-0.1.0/pillarz/_auth/flows.py +123 -0
- pillarz-0.1.0/pillarz/_auth/identity.py +66 -0
- pillarz-0.1.0/pillarz/_auth/linking.py +253 -0
- pillarz-0.1.0/pillarz/_auth/oidc.py +482 -0
- pillarz-0.1.0/pillarz/_auth/oidc_accounts.py +197 -0
- pillarz-0.1.0/pillarz/_auth/oidc_routes.py +363 -0
- pillarz-0.1.0/pillarz/_auth/passwords.py +146 -0
- pillarz-0.1.0/pillarz/_auth/roles.py +64 -0
- pillarz-0.1.0/pillarz/_auth/routes.py +398 -0
- pillarz-0.1.0/pillarz/_auth/secretbox.py +52 -0
- pillarz-0.1.0/pillarz/_auth/sessions.py +514 -0
- pillarz-0.1.0/pillarz/_auth/setup.py +217 -0
- pillarz-0.1.0/pillarz/_auth/stepup.py +46 -0
- pillarz-0.1.0/pillarz/_auth/throttle.py +193 -0
- pillarz-0.1.0/pillarz/_auth/tokens.py +126 -0
- pillarz-0.1.0/pillarz/_db/__init__.py +1 -0
- pillarz-0.1.0/pillarz/_db/clock.py +67 -0
- pillarz-0.1.0/pillarz/_db/connection.py +338 -0
- pillarz-0.1.0/pillarz/_db/m0001_initial.py +194 -0
- pillarz-0.1.0/pillarz/_db/m0002_sessions.py +19 -0
- pillarz-0.1.0/pillarz/_db/m0003_oidc_origin.py +27 -0
- pillarz-0.1.0/pillarz/_db/migrations.py +274 -0
- pillarz-0.1.0/pillarz/_events.py +128 -0
- pillarz-0.1.0/pillarz/_i18n/__init__.py +11 -0
- pillarz-0.1.0/pillarz/_i18n/babel-js.cfg +4 -0
- pillarz-0.1.0/pillarz/_i18n/babel.cfg +8 -0
- pillarz-0.1.0/pillarz/_i18n/catalogs.py +221 -0
- pillarz-0.1.0/pillarz/_i18n/jinja.py +41 -0
- pillarz-0.1.0/pillarz/_i18n/js.py +43 -0
- pillarz-0.1.0/pillarz/_i18n/request.py +113 -0
- pillarz-0.1.0/pillarz/_i18n/tools.py +200 -0
- pillarz-0.1.0/pillarz/_protocol.py +351 -0
- pillarz-0.1.0/pillarz/_runtime.py +86 -0
- pillarz-0.1.0/pillarz/_settings.py +272 -0
- pillarz-0.1.0/pillarz/_themes.py +218 -0
- pillarz-0.1.0/pillarz/_web/__init__.py +1 -0
- pillarz-0.1.0/pillarz/_web/app.py +82 -0
- pillarz-0.1.0/pillarz/_web/context.py +14 -0
- pillarz-0.1.0/pillarz/_web/csp.py +148 -0
- pillarz-0.1.0/pillarz/_web/errors.py +169 -0
- pillarz-0.1.0/pillarz/_web/gate.py +150 -0
- pillarz-0.1.0/pillarz/_web/guard.py +253 -0
- pillarz-0.1.0/pillarz/_web/headers.py +63 -0
- pillarz-0.1.0/pillarz/_web/net.py +48 -0
- pillarz-0.1.0/pillarz/_web/pages.py +30 -0
- pillarz-0.1.0/pillarz/_web/render.py +341 -0
- pillarz-0.1.0/pillarz/_web/static.py +40 -0
- pillarz-0.1.0/pillarz/accounts.py +106 -0
- pillarz-0.1.0/pillarz/config.py +377 -0
- pillarz-0.1.0/pillarz/db.py +375 -0
- pillarz-0.1.0/pillarz/events.py +228 -0
- pillarz-0.1.0/pillarz/i18n.py +126 -0
- pillarz-0.1.0/pillarz/locale/en/LC_MESSAGES/pillarz-js.po +1119 -0
- pillarz-0.1.0/pillarz/locale/en/LC_MESSAGES/pillarz.po +1894 -0
- pillarz-0.1.0/pillarz/locale/pillarz-js.pot +1106 -0
- pillarz-0.1.0/pillarz/locale/pillarz.pot +1816 -0
- pillarz-0.1.0/pillarz/settings.py +33 -0
- pillarz-0.1.0/pillarz/static/pillarz/css/base.css +587 -0
- pillarz-0.1.0/pillarz/static/pillarz/favicon.svg +1 -0
- pillarz-0.1.0/pillarz/static/pillarz/js/account.js +337 -0
- pillarz-0.1.0/pillarz/static/pillarz/js/admin-links.js +158 -0
- pillarz-0.1.0/pillarz/static/pillarz/js/admin.js +627 -0
- pillarz-0.1.0/pillarz/static/pillarz/js/api.js +96 -0
- pillarz-0.1.0/pillarz/static/pillarz/js/forms.js +206 -0
- pillarz-0.1.0/pillarz/static/pillarz/js/i18n.js +158 -0
- pillarz-0.1.0/pillarz/static/pillarz/js/token.js +148 -0
- pillarz-0.1.0/pillarz/static/pillarz/js/ui.js +540 -0
- pillarz-0.1.0/pillarz/static/pillarz/vendor/lucide/LICENSE +43 -0
- pillarz-0.1.0/pillarz/templates/pillarz/account.html +26 -0
- pillarz-0.1.0/pillarz/templates/pillarz/admin.html +48 -0
- pillarz-0.1.0/pillarz/templates/pillarz/bare.html +25 -0
- pillarz-0.1.0/pillarz/templates/pillarz/base.html +38 -0
- pillarz-0.1.0/pillarz/templates/pillarz/error.html +15 -0
- pillarz-0.1.0/pillarz/templates/pillarz/icons.svg +66 -0
- pillarz-0.1.0/pillarz/templates/pillarz/login.html +39 -0
- pillarz-0.1.0/pillarz/templates/pillarz/partials/account/delete.html +5 -0
- pillarz-0.1.0/pillarz/templates/pillarz/partials/account/devices.html +9 -0
- pillarz-0.1.0/pillarz/templates/pillarz/partials/account/language.html +15 -0
- pillarz-0.1.0/pillarz/templates/pillarz/partials/account/login.html +56 -0
- pillarz-0.1.0/pillarz/templates/pillarz/partials/account/look.html +17 -0
- pillarz-0.1.0/pillarz/templates/pillarz/partials/account/profile.html +17 -0
- pillarz-0.1.0/pillarz/templates/pillarz/partials/flash.html +8 -0
- pillarz-0.1.0/pillarz/templates/pillarz/partials/nav.html +34 -0
- pillarz-0.1.0/pillarz/templates/pillarz/plain.html +16 -0
- pillarz-0.1.0/pillarz/templates/pillarz/setup.html +51 -0
- pillarz-0.1.0/pillarz/templates/pillarz/token.html +22 -0
- pillarz-0.1.0/pillarz/testing/__init__.py +236 -0
- pillarz-0.1.0/pillarz/testing/_web.py +113 -0
- pillarz-0.1.0/pillarz/testing/fakeidp.py +362 -0
- pillarz-0.1.0/pillarz/text.py +81 -0
- pillarz-0.1.0/pillarz/web.py +246 -0
- pillarz-0.1.0/pillarz.egg-info/PKG-INFO +71 -0
- pillarz-0.1.0/pillarz.egg-info/SOURCES.txt +120 -0
- pillarz-0.1.0/pillarz.egg-info/dependency_links.txt +1 -0
- pillarz-0.1.0/pillarz.egg-info/requires.txt +10 -0
- pillarz-0.1.0/pillarz.egg-info/top_level.txt +1 -0
- pillarz-0.1.0/pyproject.toml +58 -0
- pillarz-0.1.0/requirements.in +13 -0
- pillarz-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Alle nennenswerten Änderungen an pillarz. Dieses CHANGELOG richtet sich an **App-Entwickler**
|
|
4
|
+
(Breaking Changes, neue Hooks, Upgrade-Hinweise), nicht an Endnutzer. Format nach
|
|
5
|
+
[Keep a Changelog](https://keepachangelog.com/de/1.1.0/), Versionen nach SemVer (ADR 0002): vor 1.0
|
|
6
|
+
darf sich die öffentliche API zwischen 0.x-Versionen ohne Deprecation-Frist ändern, jede solche
|
|
7
|
+
Änderung steht unter „Breaking“. Abschnitte: Added, Changed, Deprecated, Removed, Fixed, Security,
|
|
8
|
+
Breaking, Upgrade.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.1.0] — 2026-10-30
|
|
13
|
+
|
|
14
|
+
**0.1.0 – Fundament.** Gegenüber 0.1.0.dev1 (Gerüst, Datenbank, Migrationen, Web-Rahmen, i18n) kommen
|
|
15
|
+
Sitzungen und Passwort-Anmeldung, die Ersteinrichtung, Konten mit Rollen und Events, Admin-Oberfläche und
|
|
16
|
+
Kontoseite, Einstellungen, authentik (OIDC) mit Verknüpfung und RP-Logout sowie die App-Hooks dazu. Damit deckt
|
|
17
|
+
pillarz ab, was labelz heute in `security.py`, `oidc.py`, `linking.py`, `events.py`, `themes.py`, im allgemeinen
|
|
18
|
+
Teil von `settings.py` und in den Konten-/Admin-Teilen von `api/` hat. Nachweis: `examples/minimal` und die
|
|
19
|
+
Referenz-Umstellung `tests/adoption/labelz/` (siehe Upgrade). Breaking-Abschnitt in zwei Teilen: gegenüber
|
|
20
|
+
0.1.0.dev1 und gegenüber dem heutigen Verhalten von labelz.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
**Sitzungen und Passwort (ADR 0005)**
|
|
25
|
+
- Serverseitige Sitzungen in `pz_sessions`: Cookie `__Host-<name>` bei reinem HTTPS (sonst `<name>`), `HttpOnly`,
|
|
26
|
+
`SameSite=Lax`, 256 Bit Zufall, in der Datenbank nur der SHA-256. Eine Abfrage je Request (statische Dateien
|
|
27
|
+
ohne), absolute Dauer `SESSION_DAYS`, optionaler Leerlauf `IDLE_TIMEOUT_MINUTES` (0 = aus, sonst mindestens 5;
|
|
28
|
+
endet nie zu früh, höchstens eine Minute zu spät), `last_seen_at` höchstens 1×/60 s; Anfragen mit
|
|
29
|
+
`X-PZ-Background: 1` (`api(…, {background: true})`) verlängern nicht. Neue Sitzung bei jeder Anmeldung
|
|
30
|
+
(Fixation), gesperrte Konten sind sofort abgemeldet.
|
|
31
|
+
- **Aufräumen beim Start** (P-43): abgelaufene und wegen Leerlauf ungültige Sitzungen, abgelaufene Flows, alte
|
|
32
|
+
Bremsen-Einträge und erledigte Einmal-Tokens (> 30 Tage) werden gelöscht – still, `user.signed_out` mit
|
|
33
|
+
`reason="expired"` folgt mit 0.2.
|
|
34
|
+
- Fremde oder ungültige Cookie-Werte unter dem Cookienamen (z. B. labelz' Starlette-Cookie `labelz`, Müll,
|
|
35
|
+
gelöschte/abgelaufene Sitzungen) werden still verworfen: anonym, Antwort löscht das Cookie, kein
|
|
36
|
+
Protokolleintrag (P-106).
|
|
37
|
+
- `pillarz.web.session_id(request)`: öffentliche, stabile Kennung der Sitzung (`pz_sessions.public_id`, nicht
|
|
38
|
+
der Token-Hash), neu nach jeder Anmeldung, `None` ohne Sitzung (P-104).
|
|
39
|
+
- Seiten `GET/POST /login`, `POST /logout`, `GET/POST /setup` (ohne JavaScript) und JSON `POST
|
|
40
|
+
/api/pz/me/password`, `POST /api/pz/me/logout-all` (`{"keep_current": true}`), `POST /api/pz/me/reauth`,
|
|
41
|
+
`GET /api/pz/me/sessions`, `DELETE /api/pz/me/sessions/{session_id}`. `/api/pz/me/*` nimmt nur Sitzungen an.
|
|
42
|
+
`/login`, `/logout`, `/setup`, `/auth/` und `/api/pz/` sind `no-store` (Referrer-Policy wie überall
|
|
43
|
+
`same-origin`).
|
|
44
|
+
- Kopfzeile für Abgemeldete: „Anmelden“ bzw. „Einrichten“, solange `/setup` offen ist (auch auf Fehlerseiten,
|
|
45
|
+
nicht auf `/login`/`/setup` selbst).
|
|
46
|
+
- Protokoll auf der Konsole: hat niemand Logging eingerichtet (root-Logger ohne Handler, z. B. nur uvicorn),
|
|
47
|
+
schreibt pillarz die Einträge von `pillarz` und dem App-Logger ab INFO einzeilig auf stderr – so sieht der
|
|
48
|
+
Betreiber im Container z. B. die Warnung „Einrichtung offen“. Mit eigener Logging-Konfiguration nicht.
|
|
49
|
+
- Passwortregeln K-4 (10–200 Zeichen, nicht gleich dem Benutzernamen, kein Platzhalter, App-Liste
|
|
50
|
+
`Pillarz(weak_passwords=…)`); scrypt wie labelz mit Rehash schwächerer Parameter bei der Anmeldung.
|
|
51
|
+
- **Anmeldebremse in der Datenbank** (`pz_throttle`, über alle Worker/Container): 5 Fehlversuche je IP
|
|
52
|
+
(IPv6 je /64) und je Benutzername in 10 min, 100 insgesamt; für Anmeldung, Passwortwechsel, `reauth`.
|
|
53
|
+
`pillarz.web.throttled(key, limit, window_seconds)` und `hit(key)` für eigene Bremsen der App (eigener
|
|
54
|
+
Namensraum, Fenster ≤ 24 h).
|
|
55
|
+
- Schritt-hoch (Server-Teil): `reauth_at` je Sitzung, frisch für 5 min nach Anmeldung oder `POST
|
|
56
|
+
/api/pz/me/reauth`; pillarz-Aktionen antworten sonst 403 mit `reauth` (Methoden). Für App-Routen noch nicht
|
|
57
|
+
öffentlich.
|
|
58
|
+
- Geräteliste: Gerät aus dem User-Agent („Firefox auf Linux“), Methode, angemeldet seit, zuletzt aktiv, IP,
|
|
59
|
+
„dieses Gerät“.
|
|
60
|
+
- Kurzlebiger Vorab-Zustand für Anmeldeabläufe (`pz_flows`, Cookie `<name>_flow`, einmalig, 10 min).
|
|
61
|
+
|
|
62
|
+
**Ersteinrichtung (ADR 0015)**
|
|
63
|
+
- `/setup` ist offen nur ohne `ADMIN_PASSWORD`, mit Methode `password` und ohne Konten; `/login` leitet dann
|
|
64
|
+
dorthin, danach antwortet `/setup` 404 und leitet nach dem Anlegen auf `/admin`. Atomar über
|
|
65
|
+
`pz_meta.setup_done` (zwei gleichzeitige Versuche → ein Admin, der zweite 409). Optional `SETUP_TOKEN`
|
|
66
|
+
(konstante Zeit, gebremst). Startwarnung (Stufe Warnung) und stündliche Wiederholung, solange offen;
|
|
67
|
+
Warnhinweis auf der Seite bei `https://`-Adresse ohne Token.
|
|
68
|
+
- Erstes Admin-Konto aus `ADMIN_USERNAME`/`ADMIN_PASSWORD` beim Start (nur leere Datenbank).
|
|
69
|
+
|
|
70
|
+
**Konten, Rollen, Events (ADR 0006)**
|
|
71
|
+
- Rollen `admin`, `user`, `guest`. `pillarz.accounts` (lesend): `User`, `get_user`, `find_user`, `list_users`,
|
|
72
|
+
`count_active_admins`. `require_member` (user + admin) für normale App-Seiten, `require_user` lässt auch Gäste
|
|
73
|
+
durch; `require_admin`/`is_admin` rechnen mit der wirksamen Rolle (befristete authentik-Admin-Rechte der
|
|
74
|
+
Sitzung, Deckel `max_role` eines Authenticators).
|
|
75
|
+
- `pz.on(event, order=…)`: tx-Events `user.created`, `user.updated`, `user.deactivating`, `user.reactivated`,
|
|
76
|
+
`user.deleting` (in der pillarz-Transaktion, `ev.conn`; `ev.veto(text)` bei created/deactivating/deleting →
|
|
77
|
+
409, alles zurückgerollt, auch Schreibzugriffe anderer Handler), Nach-Commit-Events `user.deleted` (`result`),
|
|
78
|
+
`user.signed_in` (`session_id`, `method`), `user.signed_out` (`session_ids`, `reason`) – Fehler dort werden
|
|
79
|
+
protokolliert und geschluckt (K-8). `user.created` mit `source` = `admin`, `setup`, `env`, `cli`, `oidc`.
|
|
80
|
+
`user.delete_preview` (Hinweiszeilen, `ev.block(text)`; läuft beim Löschen ein zweites Mal in der
|
|
81
|
+
Lösch-Transaktion – rein lesend halten). Nutzlasten in `pillarz.events` (`READ_EVENTS` exportiert). Derselbe
|
|
82
|
+
Handler zweimal für dasselbe Event → `ValueError`.
|
|
83
|
+
- `connect()` in einem tx-Event-Handler liefert auf demselben Thread die laufende Transaktion (auch
|
|
84
|
+
verschachtelt). Threads in tx-Handlern sind verboten: andere Threads warten höchstens `TX_WAIT_SECONDS` (10 s)
|
|
85
|
+
und bekommen dann `pillarz.db.TxThreadError` statt zu verklemmen.
|
|
86
|
+
- `pz.delete_input(DeleteInput(key, kind="user|choice|bool|text", label, required, default, help, options,
|
|
87
|
+
on_self, self_default, max_length, visible))` – Zusatzangaben im Löschdialog, serverseitig geprüft, als
|
|
88
|
+
`ev.inputs` (`MappingProxyType`). `kind="user"` wird bei Selbstlöschung nie abgefragt und braucht dann
|
|
89
|
+
`self_default` (Wert oder `fn(user, conn)`), sonst bricht `create_app()` ab; `visible=fn(user, conn)` fragt
|
|
90
|
+
nur, wenn es etwas zu entscheiden gibt.
|
|
91
|
+
- **Konto selbst löschen** (F-3): `Pillarz(self_delete=True)` (Standard), `GET /api/pz/me/delete`, `DELETE
|
|
92
|
+
/api/pz/me` mit `{"inputs", "confirm": <benutzername>}` – Schritt-hoch, letzter aktiver Admin gesperrt, Cookie
|
|
93
|
+
gelöscht, Ziel `/login?hint=account-deleted` (bzw. RP-Logout). `self_delete=False` → Abschnitt fehlt,
|
|
94
|
+
Endpunkte 404.
|
|
95
|
+
|
|
96
|
+
**Admin-Oberfläche und Kontoseite**
|
|
97
|
+
- `/admin` mit Reitern Konten (anlegen, bearbeiten, Rolle, sperren, Passwort setzen, Sitzungen beenden,
|
|
98
|
+
Löschdialog mit Zusatzangaben, Vorschau und „Stattdessen sperren“), authentik-Links, Geräte, Themes,
|
|
99
|
+
Einstellungen, Protokoll (Filter Bereich, Konto, nur Probleme, Blättern); Hash-Routing `/admin#<key>[-<id>]`.
|
|
100
|
+
`pz.admin_tab(AdminTab(key, label, order, icon, template, module, visible))` – ES-Modul mit `init(panel, ctx)`;
|
|
101
|
+
belegte Schlüssel: `users`, `links`, `sessions`, `themes`, `settings`, `log`.
|
|
102
|
+
- `/account` (alle Rollen): Profil, Darstellung (Theme, hell/dunkel/auto), Sprache, Anmeldung (Passwort,
|
|
103
|
+
authentik), Geräte, Konto löschen; `pz.account_section(AccountSection(…))` mit `init(section, ctx)`. Abschnitte
|
|
104
|
+
inaktiver Methoden fehlen, ihre Endpunkte antworten 404. `GET/PATCH /api/pz/me`, `GET /api/pz/themes`.
|
|
105
|
+
- Admin-API unter `/api/pz/admin/…` (Konten, Sitzungen, Themes, Einstellungen, Protokoll); `GET
|
|
106
|
+
/api/pz/admin/users` liefert `elevated_only`, `…/users/{id}/delete` `reauth` (`{needed, methods}`).
|
|
107
|
+
|
|
108
|
+
**Einstellungen**
|
|
109
|
+
- `pz.setting(Setting(key, default, kind="text|int|bool|choice|custom", label, help, ui, validate, options,
|
|
110
|
+
required, max_length, min, max))`; `kind="custom"` verlangt `validate`. Öffentlich
|
|
111
|
+
`pillarz.settings.get/all/update` (`SettingError` mit `key`). pillarz-Schlüssel `site_name`, `default_theme`,
|
|
112
|
+
`default_locale` wirken im Seitenrahmen (Zwischenspeicher je Prozess, höchstens 5 s alt). Meldungen am Feld,
|
|
113
|
+
Werte werden nie still gekappt; „alt → neu“ im Protokoll.
|
|
114
|
+
- `pz.settings_section(SettingsSection(key, label, order, keys, template, module))` – App-Abschnitt im Reiter
|
|
115
|
+
„Einstellungen“, speichert über `ctx.save()` (`PATCH /api/pz/admin/settings`, 422 mit `field`); ein Schlüssel
|
|
116
|
+
in zwei Abschnitten oder ein nicht angemeldeter Schlüssel → Startabbruch.
|
|
117
|
+
|
|
118
|
+
**authentik / OIDC (ADR 0008)**
|
|
119
|
+
- Anmeldung über `/auth/login` → `/auth/callback` (Authorization Code + PKCE, `state`/`nonce` in `pz_flows`,
|
|
120
|
+
ID-Token mit den Verfahren der Discovery – HS256 nur, wenn der Anbieter es meldet –, Userinfo). **Kontozuordnung
|
|
121
|
+
nur über die verknüpfte `sub`, nie über die E-Mail-Adresse** (Leitung 2026-10-30); unbekannte `sub` → Anlage
|
|
122
|
+
bei Gruppe (Adresse aus authentik, wenn frei; bestätigt nur bei `email_verified`) → sonst 403. Gruppen
|
|
123
|
+
`OIDC_USER_GROUPS` (`user`), **neu** `OIDC_GUEST_GROUPS` (`guest`), `OIDC_ADMIN_GROUPS` (befristete
|
|
124
|
+
Admin-Rechte je Sitzung, `OIDC_ADMIN_HOURS`; die Rolle am Konto ändert die Anmeldung nie). Knopf „Mit
|
|
125
|
+
<OIDC_LABEL> anmelden“, `#boot.auth` (`{"password": bool, "oidc": {"enabled", "label"}}`).
|
|
126
|
+
- pillarz-Migration `0003_users_oidc_origin`: `users.created_via` (Quelle der Anlage wie `user.created`
|
|
127
|
+
`source`; `NULL` = vorher/übernommen = lokal) und `users.oidc_unlinked_sub` (private Spalten).
|
|
128
|
+
- **RP-initiated Logout:** `POST /logout` einer authentik-Sitzung leitet zu `end_session_endpoint` mit
|
|
129
|
+
`id_token_hint`, `client_id` und `post_logout_redirect_uri=<PUBLIC_URL>/login?hint=logged-out`; JSON-Antworten von
|
|
130
|
+
`DELETE /api/pz/me` (`hint=account-deleted`) und `DELETE /api/pz/me/sessions/{id}` für die **aktuelle** Sitzung
|
|
131
|
+
tragen das Ziel in `redirect`. GET, weil authentik POST mit 403 (CSRF) ablehnt; Restrisiko ADR 0008. Schalter
|
|
132
|
+
`OIDC_LOGOUT` (Standard 1). „Überall abmelden“ und fremde Sitzungen beenden bleibt lokal.
|
|
133
|
+
- **Schritt-hoch per authentik:** `/auth/login?reauth=1&next=…` mit `prompt=login`, `max_age=300` (authentik 2026.8
|
|
134
|
+
wertet `max_age=0` nicht aus und erzwingt `prompt=login` nur einmal je Sitzung, ADR 0008); frisch nur bei `auth_time`
|
|
135
|
+
≤ 5 min (+ 60 s Spielraum) und demselben Konto – eine jüngere authentik-Anmeldung reicht ohne neue Abfrage. Hat ein
|
|
136
|
+
Konto Passwort und authentik, bietet der Dialog beide Wege („Mit <OIDC_LABEL> bestätigen“).
|
|
137
|
+
- **Falscher `OIDC_ISSUER` ist auffindbar:** Discovery-Prüfung beim Start im Hintergrund (Timeout 5 s, hält den Start
|
|
138
|
+
nicht auf, bricht ihn nicht ab, nicht im Testmodus). Jeder Fehlschlag steht mit Adresse und Status im Protokoll,
|
|
139
|
+
getrennt nach falsch eingerichtet (404, anderer Issuer, kein Discovery-Dokument, Zertifikat) und nicht erreichbar
|
|
140
|
+
(Verbindung, Timeout, 5xx), je Ursache höchstens einmal pro Minute. `/admin` zeigt dazu einen Hinweis; `/auth/login`
|
|
141
|
+
meldet bei falscher Einstellung „nicht eingerichtet“ statt „nicht erreichbar“.
|
|
142
|
+
- Kontenliste: Chip „Admin, befristet bis HH:MM“ für Nutzer und Gäste mit laufenden befristeten Admin-Rechten; neues
|
|
143
|
+
Feld `elevated_until` in `GET /api/pz/admin/users`.
|
|
144
|
+
- **Zertifikate:** Die OIDC-Abrufe vertrauen dem Zertifikatsspeicher des Systems statt dem certifi-Bündel von httpx,
|
|
145
|
+
`OIDC_CA_FILE` kommt zusätzlich dazu.
|
|
146
|
+
- **Verknüpfung** (labelz E-35–E-37): Admin-Einmal-Link (`pz_tokens`, Art `oidc_link`, 7 Tage, nur im Fragment
|
|
147
|
+
`/auth/link#…`, optional erwarteter Benutzername), Einlöse-Seite, `POST /api/pz/link/info|start`, `POST/DELETE
|
|
148
|
+
/api/pz/admin/users/{id}/oidc-link`, `PATCH …/users/{id}` mit `oidc_unlink`; Selbst verknüpfen/lösen auf der
|
|
149
|
+
Kontoseite (`POST /api/pz/me/oidc-link|oidc-unlink` mit Passwort). Beide Verknüpfungswege verlangen bei authentik
|
|
150
|
+
eine neue Anmeldung (`prompt=login`, `max_age=60`). **Über authentik entstandene Konten (`created_via = 'oidc'`) lassen sich
|
|
151
|
+
nicht lösen** – 409, die UI bietet es nicht an („Zugang entziehen = Konto sperren“); `oidc_created` in den
|
|
152
|
+
Konto-JSONs. Nach dem Lösen legt dieselbe `sub` kein neues Konto an, bis sie wieder verknüpft ist.
|
|
153
|
+
|
|
154
|
+
**Hooks für Apps**
|
|
155
|
+
- **`pz.context(fn)`** (P-9): `fn(request, user) -> dict` liefert Werte für den Template-Kontext jeder Seite
|
|
156
|
+
und für Routen über `pillarz.web.request_context(request)` (danach auch `request.state.app_context`) – z. B.
|
|
157
|
+
Navigations-Badges wie labelz' `nav_answers`. Nur für Angemeldete, höchstens einmal je Request und erst bei
|
|
158
|
+
Bedarf (erstes `render()`/`request_context()`). Mehrere Haken in Anmeldereihenfolge, `render(**ctx)` hat
|
|
159
|
+
Vorrang; von pillarz belegte Schlüssel → Fehler. Eine Ausnahme im Haken ergibt 500 mit Fehlerseite und
|
|
160
|
+
Protokolleintrag.
|
|
161
|
+
- **Request-Authenticators** (P-99): `@pz.request_authenticator(name, max_role="user", methods=("GET","HEAD"))`
|
|
162
|
+
mit Rückgabe `None`, Konto-ID oder `pillarz.web.AuthResult(user_id, context)`; `pillarz.web.auth_context(request)`.
|
|
163
|
+
Läuft vor dem Gate, nur ohne Sitzung, sieht den Pfad; Rolle gedeckelt, pillarz-Seiten und `/api/pz/*` → 403,
|
|
164
|
+
keine Nebenwirkungen (keine Sitzung, kein Cookie, kein `last_seen`, kein `user.signed_in`).
|
|
165
|
+
- `pz.secret_user_column(name)` (S-N6): Spalte von `users` als geheim melden – sie erscheint nie in Kontozeile,
|
|
166
|
+
Templates oder JSON.
|
|
167
|
+
- `pillarz.db`: `scalar`, `execute`, `TxThreadError`; jedes Symbol in `__all__` mit Docstring (P-100).
|
|
168
|
+
- `ui.js`: `numProblem(input)`, `fieldError(input, text)`/`clearFieldError(input)` (mit `aria-invalid`,
|
|
169
|
+
`aria-describedby`), `validationMessage(input)` (`data-msg-required|pattern|type`), `clearToasts(type)`;
|
|
170
|
+
`ui.dialog()` meldet ungültige Felder am ersten Feld. Theme-Token `--pz-input-line` (≥ 3:1) und
|
|
171
|
+
`--pz-accent-text` (≥ 4,5:1), `/themes.css` liefert `--t-accent-text` je Theme und Modus.
|
|
172
|
+
|
|
173
|
+
**Testkit, Beispiel, Referenz-Umstellung, CI**
|
|
174
|
+
- `pillarz.testing.make_user()` (liefert `User`), `login()` (echter `POST /login`), `login_as()` (Sitzung direkt
|
|
175
|
+
in der Datenbank, echte `Set-Cookie`-Antwort), `PASSWORD`; `FakeIdP` (simulierter authentik: Discovery, JWKS,
|
|
176
|
+
Token, Userinfo, `end_session_endpoint` mit Prüfung von `id_token_hint` (POST → 403 wie authentik), `auth_time`;
|
|
177
|
+
`max_age` und `prompt=login` entscheidet er wie authentik 2026.8 (`markers`, `login_ids`, `logins`);
|
|
178
|
+
prüft `response_type`, `scope`, `code_challenge_method`; Schalter `algs`, `auth_time`,
|
|
179
|
+
`registered_post_logout`, `end_session_post`; `idp.env()`, `idp.install()`, `idp.serve()`),
|
|
180
|
+
`oidc_login(client, idp, claims, next=…, reauth=…)`, `oidc_callback(…)`.
|
|
181
|
+
- `examples/minimal`: `require_member`, `/password`, `/notes`, Events mit Zusatzangabe `heir` (nur bei
|
|
182
|
+
freigegebenen Designs), Vorschau und Veto, App-Reiter „Designs“, Einstellungs-Abschnitt „Notizen“,
|
|
183
|
+
App-Einstellungen `greeting` und `designs_per_page`, `auth_methods=("password", "oidc")`.
|
|
184
|
+
- **Referenz-Umstellung labelz, Schritt 1** (P-84) in `tests/adoption/labelz/`: synthetische labelz-0.13-
|
|
185
|
+
Datenbank, Vorlage der App-Migration (`migration.py`) und eine Mini-App mit labelz' Andockpunkten; läuft auf
|
|
186
|
+
SQLite und PostgreSQL (siehe Upgrade).
|
|
187
|
+
- CI: zusätzlicher Testlauf mit **Python 3.13** (SQLite, gleiche Hash-Pins; 3.12 bleibt Hauptversion für Locks,
|
|
188
|
+
PostgreSQL-Job und Build), Build wartet auf ihn (P-87).
|
|
189
|
+
- `python -m tests.browser_smoke`: Ersteinrichtung, Anmelden, Kontoseite, Admin-Reiter, Nutzer-Sicht,
|
|
190
|
+
Selbstlöschung, Passwort ändern, Abmelden per Tastatur; zweite Instanz mit authentik gegen `FakeIdP`
|
|
191
|
+
(RP-Logout, Admin-Dialoge Link erstellen/zurückziehen/Verknüpfung lösen).
|
|
192
|
+
|
|
193
|
+
### Changed
|
|
194
|
+
- `require_*`, `current_user`, `is_admin`, `via` arbeiten auf der echten Sitzung (in 0.1.0.dev1 waren sie
|
|
195
|
+
Hüllen ohne Anmeldung) – siehe Breaking.
|
|
196
|
+
- HTML-Formulare (Nicht-GET außerhalb `/api/` mit `Accept: text/html`) bekommen bei Ablehnung oder Fehler die
|
|
197
|
+
Fehlerseite statt JSON – auch bei frühen Ablehnungen des Guards (CSRF, Größe); 401 → `/login`. `fetch` mit
|
|
198
|
+
`Accept: application/json` und `/api/` bleiben JSON.
|
|
199
|
+
- Fehlerseiten: eigener Titel je Code („Kein Zugriff“, „Seite nicht gefunden“, „Gerade nicht erreichbar“),
|
|
200
|
+
Erklärung mit nächstem Schritt statt Standardmeldung (eigene Meldungen der App bleiben); angemeldet zeigen
|
|
201
|
+
403/404 die Navigation.
|
|
202
|
+
- Toasts: gleiche Texte zusammengefasst (×N), höchstens zwei, Erfolg 4 s; Fehler am Feld entfernen offene
|
|
203
|
+
Erfolgsmeldungen.
|
|
204
|
+
- Die einmalige Rückmeldung (Flash) wird erst verbraucht, wenn eine Seite mit Rahmen sie zeigt.
|
|
205
|
+
- Protokolltexte der Anmeldung enthalten die IP nicht mehr; sie steht nur in der Spalte `ip`.
|
|
206
|
+
- Leeres `PUBLIC_URLS` warnt im Testmodus nicht mehr (gemischt http/https weiterhin immer).
|
|
207
|
+
- `env.example` vollständig für 0.1.0 (Test: jede gelesene Variable steht dort).
|
|
208
|
+
|
|
209
|
+
### Fixed
|
|
210
|
+
- Firefox/Zen über http zu einer Nicht-localhost-Adresse: Anmelden und Einrichten scheiterten mit „Anfrage von
|
|
211
|
+
fremder Seite abgelehnt.“ – `Referrer-Policy: no-referrer` auf no-store-Seiten ließ Firefox beim Formular-POST
|
|
212
|
+
`Origin: null` senden (ohne `Sec-Fetch-Site`). Diese Seiten haben jetzt `same-origin`;
|
|
213
|
+
`testing.assert_security_headers` verlangt `same-origin`.
|
|
214
|
+
- `pillarz.db.reset()` (nur Tests) scheiterte auf SQLite an App-Schemata mit mehrstufigen Fremdschlüsseln auf
|
|
215
|
+
`users` („no such table: main.users“, z. B. labelz) – löscht jetzt ohne Fremdschlüsselprüfung.
|
|
216
|
+
|
|
217
|
+
### Security
|
|
218
|
+
- Gleiche Meldung und Antwortzeit für „falsch“, „unbekannt“ und „gesperrt“ (Dummy-Hash mit aktuellen
|
|
219
|
+
Parametern); gesperrtes Konto + richtiges Passwort → dasselbe 401, Grund nur im Protokoll (S-N5).
|
|
220
|
+
- Bremse ohne Wettlauf: Versuche werden vor scrypt atomar reserviert (SQLite `BEGIN IMMEDIATE`, PG
|
|
221
|
+
`pg_advisory_xact_lock`), 40 parallele Fehlversuche ergeben höchstens 5 Prüfungen. scrypt höchstens viermal
|
|
222
|
+
gleichzeitig je Prozess; sind alle Plätze > 2 s belegt: 503 mit `Retry-After` (zählt nicht als Fehlversuch).
|
|
223
|
+
- Kein Aussperren durch Fremde: Passwortbestätigungen angemeldeter Sitzungen zählen unter `confirm:<user_id>`;
|
|
224
|
+
Bremsschlüssel nur aus gültigen Benutzernamen; greift ein Konto- oder der globale Schlüssel, warnt das
|
|
225
|
+
Protokoll.
|
|
226
|
+
- Passwortwechsel rotiert das Sitzungstoken und beendet die anderen Sitzungen in einer Transaktion; ein erstes
|
|
227
|
+
eigenes Passwort (Konto ohne Passwort) verlangt eine frische Sitzung.
|
|
228
|
+
- Rollenwechsel erhöht `session_epoch`; wer zum Admin befördert wird, verliert alle Sitzungen.
|
|
229
|
+
- Admin-Aktionen „Admin-Rolle vergeben“, „Passwort/E-Mail eines Admin-Kontos ändern“ und „Konto löschen“
|
|
230
|
+
brauchen Schritt-hoch (403 mit `reauth`). **Befristete Admins** (nur authentik-Gruppe) dürfen keine
|
|
231
|
+
Admin-Konten anlegen, befördern, herabstufen, sperren, entsperren, löschen oder deren Verknüpfung ändern – Rolle
|
|
232
|
+
und Sperre eines Admin-Kontos auch dann nicht, wenn es gesperrt ist (403, Warnung im Protokoll).
|
|
233
|
+
- Letzter aktiver Admin: Herabstufen, Sperren und Löschen (auch selbst) gesperrt, gezählt mit Sperre in der
|
|
234
|
+
Transaktion. K-9: eigenes Passwort, eigene E-Mail und eigene Verknüpfung nicht über `/api/pz/admin/…` (403).
|
|
235
|
+
- **Keine authentik-Zuordnung über die E-Mail-Adresse** (authentiks Standard-Mapping meldet `email_verified`
|
|
236
|
+
immer `true` – sonst Übernahme fremder Konten, auch von Admins). Link-Prüfung gebremst (10 / 10 min je IP,
|
|
237
|
+
IPv6 je /64), Link-Anlage 20 / min je Admin.
|
|
238
|
+
- ID-Token: nur Verfahren aus `id_token_signing_alg_values_supported` (ohne Angabe nur RS256), `azp` geprüft,
|
|
239
|
+
wenn vorhanden, `nbf` mit Spielraum, `iat` Pflicht; kaputtes JWKS und Nicht-String-Token → 400 statt 500. In
|
|
240
|
+
`pz_sessions` liegt es AES-256-GCM-verschlüsselt (Schlüssel per HKDF aus `SECRET_KEY`).
|
|
241
|
+
- `/auth/callback` verbraucht einen Flow nur bei passendem `state`; ohne passenden Flow kein Angreifertext im
|
|
242
|
+
Protokoll (eine feste, entprellte Zeile), Anbieter-`error` erst nach gültigem Flow. Flows gebremst je IP (/64)
|
|
243
|
+
und insgesamt (3000 / 10 min). Einlösen, Selbst-Verknüpfen und Lösen mit `BEGIN IMMEDIATE` (kein 500 bei
|
|
244
|
+
Wettlauf). Selbst-Lösen nimmt der laufenden Sitzung befristete Admin-Rechte und das ID-Token.
|
|
245
|
+
- `OIDC_ISSUER` mit `http://` nur für localhost, sonst Startabbruch.
|
|
246
|
+
- IDs > 2⁶³−1 in Pfad, Query und `DeleteInput(kind="user")` → 404/422 statt 500.
|
|
247
|
+
- Anmeldemethode `password` aus: `POST /login`, `/api/pz/me/password`, `/api/pz/me/reauth` → 404.
|
|
248
|
+
|
|
249
|
+
### Breaking
|
|
250
|
+
|
|
251
|
+
**Gegenüber 0.1.0.dev1**
|
|
252
|
+
- `current_user()` / `request.state.user` ist ein `pillarz.accounts.User` (eingefrorene Datenklasse der
|
|
253
|
+
öffentlichen Spalten: `user.id`, `user.username`, `user.role`, `user.is_guest` …) statt eines dicts – ebenso
|
|
254
|
+
die Rückgabe von `require_*`, `user` in Templates, `NavItem.visible/badge`, `pz.boot(request, user)`. Keine
|
|
255
|
+
internen Spalten (`theme`, `mode`, `oidc_*`, `password_hash`).
|
|
256
|
+
- pillarz-Migration `0003_users_oidc_origin` ergänzt `users.created_via` und `users.oidc_unlinked_sub` (privat,
|
|
257
|
+
additiv) – App-Code, der `SELECT * FROM users` 1:1 in eigene Strukturen kopiert, sieht zwei Spalten mehr.
|
|
258
|
+
- `ui.readNum` begrenzt und rundet nicht mehr still: Werte außerhalb von `min`/`max` oder (bei `int`) mit
|
|
259
|
+
Nachkommastellen ergeben `NaN`; den Grund liefert `numProblem(input)`.
|
|
260
|
+
|
|
261
|
+
**Gegenüber labelz (für die Umstellung)**
|
|
262
|
+
- **K-4:** neue Passwörter brauchen 10–200 Zeichen (labelz: 8). Bestehende kürzere melden sich weiter an und
|
|
263
|
+
bekommen einmal den Hinweis „bitte ändere es“.
|
|
264
|
+
- **K-15:** ein schwaches `ADMIN_PASSWORD` (oder ungültiger `ADMIN_USERNAME`) bei leerer Datenbank bricht den
|
|
265
|
+
Start ab (Exit-Code ≠ 0); labelz legte dann nur kein Konto an. Ohne `ADMIN_PASSWORD` ist bei leerer Datenbank
|
|
266
|
+
`/setup` offen.
|
|
267
|
+
- **K-8:** `user.signed_out` läuft **nach** dem Commit; ein Fehler im Handler verhindert das Abmelden nicht
|
|
268
|
+
(labelz räumte im Logout-Request selbst auf).
|
|
269
|
+
- **Alle Sitzungen enden beim Umstieg einmal:** labelz' Cookie-Sitzungen gelten nicht, das alte Cookie wird
|
|
270
|
+
verworfen und gelöscht.
|
|
271
|
+
- Gesperrtes Konto + richtiges Passwort → 401 wie ein falsches Passwort (labelz: 403 „gesperrt“).
|
|
272
|
+
- Rollen statt `is_admin`: `users.role` (`admin|user|guest`); `is_admin` liest pillarz nicht.
|
|
273
|
+
- **Keine authentik-Zuordnung per E-Mail mehr** (labelz: bestätigte Adresse, `OIDC_TRUST_EMAIL`).
|
|
274
|
+
`OIDC_TRUST_EMAIL` entfällt (gesetzt → Startwarnung, keine Wirkung). Konten ohne verknüpfte `sub` verbindet nur
|
|
275
|
+
noch ein Admin-Link oder die Selbst-Verknüpfung; bis dahin endet ihre authentik-Anmeldung mit 403 (oder – mit
|
|
276
|
+
Gruppe – in einem neuen Konto).
|
|
277
|
+
- Durch authentik-Anmeldung entstandene Konten lassen sich nicht entkoppeln (labelz: mit Passwort möglich);
|
|
278
|
+
nach dem Lösen legt dieselbe `sub` kein neues Konto an (labelz: Neuanlage per Gruppe).
|
|
279
|
+
- Verknüpfen verlangt bei authentik eine neue Anmeldung (`prompt=login`, `max_age=60`); RP-Logout statt nur lokal abmelden.
|
|
280
|
+
|
|
281
|
+
### Upgrade
|
|
282
|
+
- **Von 0.1.0.dev1:** `user["x"]` → `user.x`; ob ein Konto ein Passwort hat, liefert `GET /api/pz/me`
|
|
283
|
+
(`has_password`). Eigene `kind="custom"`-Einstellungen mit `validate=` anmelden. Preview-Handler rein lesend
|
|
284
|
+
halten; Texte von Vorschau und `DeleteInput.label` mit `lazy()` und Katalogeintrag der App. Ob die Rolle eines
|
|
285
|
+
Erben passt, prüft die App im `user.deleting`-Handler.
|
|
286
|
+
- **authentik:** Das Image braucht `ca-certificates`; eine eigene CA dort einspielen oder über `OIDC_CA_FILE` angeben.
|
|
287
|
+
`OIDC_TRUST_EMAIL` aus der Umgebung entfernen. Am Provider je `PUBLIC_URLS`-Eintrag die Redirect-URI `<url>/auth/callback` und die
|
|
288
|
+
Post-Logout-Redirect-URIs `<url>/login?hint=logged-out` und `<url>/login?hint=account-deleted` eintragen;
|
|
289
|
+
ohne `PUBLIC_URLS` keine authentik-Anmeldung (Startwarnung). Apps, die authentik anbieten, nehmen `"oidc"` in
|
|
290
|
+
`auth_methods` auf.
|
|
291
|
+
- **Ersteinrichtung:** neue Instanzen ohne `ADMIN_*` erst einrichten, dann öffentlich erreichbar machen – oder
|
|
292
|
+
`SETUP_TOKEN` bzw. `ADMIN_*` setzen (`env.example`).
|
|
293
|
+
- **labelz (Vorlage `tests/adoption/labelz/migration.py`, Architektur §5.4):** App-Migrationen
|
|
294
|
+
`0001_legacy` (bisheriges `create_schema()`) und `0002_pillarz_adoption`: `is_admin = 1` → `role = 'admin'`
|
|
295
|
+
(Spalte bleibt stehen), `oidc_links` → `pz_tokens` (`kind='oidc_link'`, `data.expected_username` über
|
|
296
|
+
`json_object_sql`, dann `DROP TABLE`), `themes` → `pz_themes`, `meta['settings']` → `pz_settings`
|
|
297
|
+
(`site_name`, `default_theme` + registrierte App-Settings; `meta` selbst bleibt), `events` der letzten 30 Tage
|
|
298
|
+
→ `pz_log`. pillarz' `0001_initial` übernimmt `users` additiv (nie neu gebaut), setzt `email_confirmed = 1` für
|
|
299
|
+
vorhandene Adressen und `setup_done`. Die Vorlage setzt `created_via = 'oidc'` für reine authentik-Konten
|
|
300
|
+
(verknüpft, ohne Passwort) – diese Annahme trifft das App-Team. Der Test beweist auf SQLite und PostgreSQL: alte Passwörter (auch 8 und
|
|
301
|
+
9 Zeichen, alte scrypt-Parameter) melden sich an, authentik-Konten per `sub` (nie per Adresse), offene Links
|
|
302
|
+
bleiben einlösbar,
|
|
303
|
+
verbrauchte/abgelaufene nicht, Admin aus `is_admin`, `/setup` zu, Themes/Einstellungen/Protokoll übernommen,
|
|
304
|
+
alle Zeilen der Fachtabellen und alle Fremdschlüssel unverändert, Trockenlauf ändert nichts. Eigene geheime
|
|
305
|
+
Altspalten mit `pz.secret_user_column(name)` melden.
|
|
306
|
+
- Apps, die bisher eigene Sitzungs-Cookies unter `<name>` setzen, verlieren diese Sitzungen einmal.
|
|
307
|
+
|
|
308
|
+
## [0.1.0.dev1] — 2026-10-29
|
|
309
|
+
|
|
310
|
+
### Added
|
|
311
|
+
- Paket und Auslieferung: `pyproject.toml` (setuptools, Version aus `pillarz.__version__`), Laufzeit-
|
|
312
|
+
Abhängigkeiten als Bereiche, exakte Locks mit Hashes (`requirements.txt`, `requirements-dev.txt`,
|
|
313
|
+
erzeugt von `scripts/lock-deps.sh`). CI baut bei jedem Push Wheel + Sdist und lädt sie bei `v*`-Tags
|
|
314
|
+
öffentlich nach **pypi.org** hoch (nur PyPI, keine Forgejo-Registry). Lizenz **`AGPL-3.0-or-later`**
|
|
315
|
+
(`LICENSE` in Repo, sdist und Wheel; SPDX-Ausdruck nach PEP 639), Kurzbeschreibung `README.md`.
|
|
316
|
+
- `pillarz.Pillarz(name=…, title=…, version=…, package_dir=…, migrations=…, auth_methods=…,
|
|
317
|
+
weak_passwords=…)` mit `create_app()`, `on_startup()`, `on_shutdown()`, `migrate()` und `config`.
|
|
318
|
+
Genau eine Instanz je Prozess; Registrieren nach `create_app()` wirft `RuntimeError`. Datenklassen
|
|
319
|
+
`NavItem`, `AdminTab`, `AccountSection`, `DeleteInput`, `Setting`, `Theme` vorerst nur als Hüllen
|
|
320
|
+
(Felder folgen mit 0.1-Paketen B–F).
|
|
321
|
+
- `pillarz.config.Config.from_env()`: alle 0.1-Variablen (siehe `env.example`), gelesen beim Erzeugen
|
|
322
|
+
von `Pillarz`, nicht beim Import. Startabbruch (`ConfigError`) bei `SECRET_KEY` < 32 Zeichen,
|
|
323
|
+
`SETUP_TOKEN` < 16 Zeichen, ungültigen `PUBLIC_URLS` oder ohne aktive Anmeldemethode; harmlose
|
|
324
|
+
Fehler als Warnung in `Config.warnings` (beim Start protokolliert).
|
|
325
|
+
- `pillarz.db`: `Conn`/`connect()` aus labelz (`?`-Platzhalter, dict-Zeilen, eine Transaktion je
|
|
326
|
+
`connect()`), DDL-Platzhalter `ddl()` (`{pk}`, `{blob}`, `{bigint}`), `now()`/`now_plus()`/`stamp()`
|
|
327
|
+
über eine zentrale Uhr, Migrations-Helfer `has_table`, `has_column`, `add_column`, `create_index`,
|
|
328
|
+
`json_object_sql`, `upsert_sql`, `applied_migrations`, dazu `IntegrityError`, `ping`, `reset`
|
|
329
|
+
(nur Tests) und die labelz-Grundbausteine `all_rows`/`one`/`insert`/`update`/`delete`/`get` mit
|
|
330
|
+
ID-Grenzen.
|
|
331
|
+
- PostgreSQL-Verbindungspool je Prozess (`PG_POOL_SIZE`, Standard 5, `0` = ohne Pool).
|
|
332
|
+
- Versionierte Migrationen je Komponente (`pz_migrations`): pillarz zuerst, dann die App; eine
|
|
333
|
+
Transaktion je Schritt, Sperre gegen parallele Worker, Startabbruch bei Fehlern und bei unbekannten
|
|
334
|
+
(neueren) Einträgen. `python -m pillarz migrate [--dry-run|--status] <modul:objekt>`.
|
|
335
|
+
- `python -m pillarz create-admin <modul:objekt>`: erstes Admin-Konto interaktiv anlegen (nur bei leerer
|
|
336
|
+
Datenbank, Passwortregeln K-4: 10–200 Zeichen, nicht gleich dem Benutzernamen, kein Platzhalter).
|
|
337
|
+
- pillarz-Migration `0001_initial`: Tabelle `users` (öffentliche Spalten `id, username, name, email,
|
|
338
|
+
role, active, locale, region, created_at, last_login, session_epoch`) und private Tabellen
|
|
339
|
+
`pz_sessions`, `pz_flows`, `pz_throttle`, `pz_tokens`, `pz_settings`, `pz_themes`, `pz_log`, `pz_meta`.
|
|
340
|
+
- `pillarz.testing`: `configure()`, `clock` (`freeze`/`advance`), `reset_runtime()` – Grundstock des
|
|
341
|
+
Testkits.
|
|
342
|
+
- `pillarz.i18n` (ADR 0011): `_`, `ngettext`, `pgettext`, `lazy`, `current_locale()` und der
|
|
343
|
+
Kontextmanager `use_locale(sprache)`. msgids sind deutsch; Kataloge
|
|
344
|
+
`<package_dir>/locale/<sprache>/LC_MESSAGES/<name>.po` (Python + Jinja) und `<name>-js.po` (JS)
|
|
345
|
+
werden ohne `.mo` direkt aus den `.po` geladen. Kette **App → pillarz → Quelltext**: eine App kann
|
|
346
|
+
pillarz-Texte mit gleicher msgid umformulieren und weitere Sprachen liefern. `fuzzy`/leere Einträge
|
|
347
|
+
zählen als fehlend. `lazy("…")` liefert ein Objekt, das erst bei `str()`/im Template übersetzt (für
|
|
348
|
+
JSON vorher `str()`).
|
|
349
|
+
- Sprachwahl je Request: Konto (`users.locale`) → `Accept-Language` → `DEFAULT_LOCALE`, jeweils nur
|
|
350
|
+
unterstützte Sprachen. Unterstützt sind `de` plus die Sprachen der App-Kataloge – hat die App keine
|
|
351
|
+
Kataloge, die von pillarz (`en`). Ohne Request (Start, Jobs) gilt die Standardsprache.
|
|
352
|
+
- Jinja: `_`, `gettext`, `ngettext`, `pgettext`, `npgettext` und `{% trans %}` in allen Templates
|
|
353
|
+
(`newstyle`: Werte als Schlüsselwörter, `{{ _("Hallo %(name)s", name=…) }}` – escaped; ein
|
|
354
|
+
wörtliches `%` als `%%`).
|
|
355
|
+
- JavaScript: `/static/pillarz/js/i18n.js` mit `t(msgid, params)`, `nt(singular, plural, n, params)`,
|
|
356
|
+
`pt(kontext, msgid, params)`; Platzhalter `{name}`, bei `nt` ist `{n}` automatisch gesetzt. Die
|
|
357
|
+
Texte kommen aus dem JSON-Block `<script type="application/json" id="i18n">` der Seite (nur die
|
|
358
|
+
JS-Domänen der aktuellen Sprache, CSP-konform); Pluralregeln aus dem Katalog, ohne `eval`.
|
|
359
|
+
- `scripts/i18n.sh extract|update|init <sprache>|check` (Babel) – auch als Vorlage für Apps
|
|
360
|
+
(`PACKAGE_DIR`, `DOMAIN`). Schlüsselwörter: Python `_`, `gettext`, `ngettext`, `pgettext`,
|
|
361
|
+
`npgettext`, `lazy`, `_l`; JS `t`, `nt`, `pt`; Übersetzerhinweise mit `# TRANSLATORS:`.
|
|
362
|
+
- Englischer Katalog für alle pillarz-Texte (`pillarz/locale/en/LC_MESSAGES/pillarz.po`).
|
|
363
|
+
- `pillarz.testing.check_catalogs(package_dir=None, name=None, *, locales=None)`: Liste fehlender,
|
|
364
|
+
`fuzzy` oder leerer Übersetzungen, fehlender Pluralformen, unpassender `%(…)`-Platzhalter und
|
|
365
|
+
fehlender Kataloge (ohne Argumente: die aktuelle App). `pillarz.testing.untranslated()`: sammelt
|
|
366
|
+
Texte, die in einer anderen Sprache als Deutsch unübersetzt angezeigt wurden.
|
|
367
|
+
- `examples/minimal`: kleinste App mit einer App-Migration.
|
|
368
|
+
- Web-Rahmen (0.1-Paket B): `create_app()` setzt Guard (reine ASGI-Middleware: Request-ID, CSRF über
|
|
369
|
+
`Sec-Fetch-Site`/`Origin`, Body-Grenzen 1 MiB bzw. Upload-Registry, 411 ohne Länge), das Gate
|
|
370
|
+
**„default deny“** (globale Dependency: jede Route ohne `Depends(pillarz.web.public)` bzw.
|
|
371
|
+
`@pillarz.web.public_route` verlangt eine Anmeldung – anonym 401, bei HTML-GET `303 /login?next=…`),
|
|
372
|
+
Sicherheits-Header auf jede Antwort (`nosniff`, `Referrer-Policy: same-origin`, CSP, HSTS nur wenn alle
|
|
373
|
+
`PUBLIC_URLS` https sind, `X-Request-ID`, `Vary: Cookie`), Fehler-Handler wie labelz (JSON für `/api/…`
|
|
374
|
+
und Nicht-GET, Fehlerseite für 403/404, Pfad-Validierung → 404, `UnicodeEncodeError` → 422,
|
|
375
|
+
`psycopg_pool.PoolTimeout` → 503 mit `Retry-After: 5`), Static mit `no-cache` + ETag unter `/static/`
|
|
376
|
+
(App) und `/static/pillarz/`, die öffentlichen Seiten `/healthz` und `/themes.css`.
|
|
377
|
+
- Neue Registrierungen an `Pillarz`: `nav(NavItem)`, `csp(**direktiven)` (nur Ergänzungen; `'unsafe-*'`
|
|
378
|
+
außer `'wasm-unsafe-eval'`, `*`, reine Schemata in `script-src`/`default-src` und Änderungen an
|
|
379
|
+
`object-src`/`base-uri`/`frame-ancestors` → `ValueError` beim Start), `upload_limit(pfad|re, bytes)`,
|
|
380
|
+
`no_store(pfad|re)`, `boot(fn)`, `log_source(logger, bereich, label)`, `theme(Theme)`, `icons(pfad)`.
|
|
381
|
+
Pfadregeln: String mit `/` am Ende = Präfix, sonst exakt; `re.compile(…)` muss den ganzen Pfad treffen.
|
|
382
|
+
- `NavItem(key, href, label, icon=None, order=100, visible=None, badge=None, badge_href=None,
|
|
383
|
+
badge_label=None)` und `Theme(id, name, light, dark)` mit Feldern.
|
|
384
|
+
- `pillarz.web`: `public`, `public_route`, `render`, `fail`, `csp_extend`, `read_form`, `client_ip`,
|
|
385
|
+
`base_url`, `current_user`, `is_admin`, `via`, `require_user`, `require_member`, `require_admin`,
|
|
386
|
+
`require_role(*rollen)`. Bis Paket D gibt es keine Sitzungen: `require_*` antworten anonym immer 401;
|
|
387
|
+
`throttled`/`hit` sind Hüllen (`NotImplementedError`).
|
|
388
|
+
- `pillarz.text`: `oneline`, `multiline`, `required_line`, `safe_next`, `to_json` (aus labelz).
|
|
389
|
+
- Templates `pillarz/base.html` (Blöcke `title`, `head`, `body_class`, `main_class`, `content`,
|
|
390
|
+
`footer_extra`, `scripts`, `icons`, dazu `topbar` und `footer`), `pillarz/error.html`,
|
|
391
|
+
`pillarz/plain.html` (Karte ohne Navigation, Blöcke `card`, `card_class`), Icon-Sprite
|
|
392
|
+
`pillarz/icons.svg` (Lucide, ISC). Template-Kette App → pillarz; Namen unter `pillarz/…` kommen immer
|
|
393
|
+
aus pillarz. Kontext: `user`, `is_admin`, `site_name`, `app_name`, `version` (anonym nur ein
|
|
394
|
+
Cache-Buster), `themes_rev`, `theme_id`, `mode`, `locale`, `region`, `nav`, `nav_items`, `flash`,
|
|
395
|
+
`boot`, `page`, `help`, `i18n`, `_`/`ngettext`/`pgettext` (Jinja-i18n, newstyle).
|
|
396
|
+
- Static: `/static/pillarz/css/base.css` (alles in `@layer pillarz`, Tokens `--pz-*` aus `--t-*`,
|
|
397
|
+
Komponenten `.btn`, `.icon-btn`, `.field`, `.banner-*`, `.card`, `.chip`, `.tabs`, `.tbl`, `.menu`,
|
|
398
|
+
`.dlg`, `.toasts`, `.topbar` …), `js/api.js` (`api`, `upload`, `postQuietly`, `ApiError`), `js/ui.js`
|
|
399
|
+
(`boot`, `pageData`, `region`, `readJson`, `h`, `icon`, `button`, `iconButton`, `toast`, `showError`,
|
|
400
|
+
`trapFocus`, `dialog`, `confirmDialog`, `promptDialog`, `field`, `numInput`, `readNum`, `selectEl`,
|
|
401
|
+
`checkbox`, `parseNum`, `fmtNum`, `fmtNumber`, `fmtDate`, `relTime`, `debounce`, `closeMenu`,
|
|
402
|
+
`menuIsOpen`, `attachMenu`, `menuButton`), `js/i18n.js` (`t`, `nt`, `pt`), `favicon.svg`.
|
|
403
|
+
- Themes: labelz' fünf eingebaute (Papier, Wald, Meer, Schiefer, Beere) + `pz.theme()` + eigene aus
|
|
404
|
+
`pz_themes`; `/themes.css` nur aus geprüften Hex-Werten, Akzentschrift nach WCAG-Kontrast.
|
|
405
|
+
- Protokoll: Einträge der Logger `pillarz` und `<name>` ab INFO in `pz_log`, mit `request_id`,
|
|
406
|
+
`actor_id` und `ip` aus dem Request (K-13); `extra={"pz_actor": id}` setzt das Konto selbst.
|
|
407
|
+
Aufbewahrung `LOG_RETENTION_DAYS` (neu, Standard 90) und höchstens 20 000 Einträge (K-14).
|
|
408
|
+
- `pillarz.testing.route_audit(app)` (jede nicht öffentliche Route anonym → 401/303→`/login`/404,
|
|
409
|
+
dazu Routen auf von pillarz belegten URLs und doppelte Routen) und `assert_security_headers(resp)`.
|
|
410
|
+
- `tests/browser_smoke.py`: manueller Browser-Smoke mit Playwright (Konsolen-/CSP-Fehler, Überlauf bei
|
|
411
|
+
360 px, Screenshots); `playwright` steht in `requirements-dev.txt`.
|
|
412
|
+
- i18n im Web-Rahmen (Paket C in B): der Guard setzt die Sprache je Request – vor CSRF/Body-Prüfung aus
|
|
413
|
+
`Accept-Language`, nach der Identität mit der Kontosprache (`user["locale"]`) – und setzt sie bewusst
|
|
414
|
+
nicht zurück, damit auch der 500er-Handler in der Sprache des Requests antwortet. Alle Templates
|
|
415
|
+
bekommen `_`, `gettext`, `ngettext`, `pgettext`, `npgettext`, `{% trans %}` und `current_locale()`;
|
|
416
|
+
`#i18n` enthält die JS-Texte der Sprache (`{"locale", "plural", "messages"}`). Kataloge werden beim
|
|
417
|
+
Start geladen (kaputte `.po` → Startabbruch). Englische Texte für alle Seiten und Meldungen aus B.
|
|
418
|
+
- **Protokoll in der Standardsprache:** `pz_log`-Einträge stehen immer in `DEFAULT_LOCALE`, nicht in
|
|
419
|
+
der Sprache des auslösenden Requests. Log-Texte als `lazy("…")` mit `%`-Argumenten loggen
|
|
420
|
+
(`log.warning(lazy("Import von %s fehlgeschlagen"), name)`) – dann sind sie extrahierbar, und der
|
|
421
|
+
Protokoll-Handler übersetzt sie beim Schreiben unter `use_locale(None)`. Andere Handler (Konsole)
|
|
422
|
+
formatieren in der aktuellen Sprache. `Config.warnings` sind weiterhin `str` mit dem deutschen
|
|
423
|
+
Quelltext und werden beim Protokollieren übersetzt.
|
|
424
|
+
- `tests/browser_smoke.py --locales de,en` (Standard): je Sprache `<html lang>`, `#i18n`-Block und
|
|
425
|
+
Übersetzung durch `i18n.js` unter der CSP.
|
|
426
|
+
- `pillarz.db.requires_fk_off` (Dekorator für Migrationsschritte) und
|
|
427
|
+
`pillarz.db.rebuild_table_sqlite(conn, table, create_sql, columns=None)` (ADR 0003): Tabellenumbau auf
|
|
428
|
+
SQLite nach dem 12-Schritte-Verfahren. Markierte Schritte laufen auf SQLite mit
|
|
429
|
+
`PRAGMA foreign_keys = OFF` (vor `BEGIN` gesetzt) und `PRAGMA foreign_key_check` vor dem Commit –
|
|
430
|
+
Verstöße rollen zurück (`MigrationError`). Ohne die Markierung verweigert `rebuild_table_sqlite` den
|
|
431
|
+
Umbau, weil `DROP TABLE` sonst `ON DELETE CASCADE` in Kindtabellen auslöst. Auf PostgreSQL hat die
|
|
432
|
+
Markierung keine Wirkung; `rebuild_table_sqlite` gibt es dort nicht (`ALTER TABLE` nutzen).
|
|
433
|
+
- `pillarz.testing.is_test_dir(pfad)`.
|
|
434
|
+
- Beim Start: Fehler im Protokoll, wenn es Konten, aber kein aktives Admin-Konto gibt und
|
|
435
|
+
`OIDC_ADMIN_GROUPS` leer ist (Aussperrung nach der Übernahme einer Datenbank sichtbar machen).
|
|
436
|
+
- **Seiten ohne Rahmen** (P-98): `render(request, template, page=…, chrome=False)` mit einem Template,
|
|
437
|
+
das `pillarz/bare.html` erweitert (Blöcke `title`, `head`, `body_class`, `content`, `scripts`; JSON
|
|
438
|
+
`#page`, `#i18n`; `data-theme`/`data-mode`/`data-version` am `<html>`). Gleiche Header und CSP wie
|
|
439
|
+
jede Seite, aber ohne Navigation, Badges, Topbar, Fuß, Flash, Toasts, Hilfe, `#boot` und ohne
|
|
440
|
+
pillarz-CSS/-JS (`base.css` bei Bedarf selbst im Block `head`). Falscher Rahmen (`base.html` mit
|
|
441
|
+
`chrome=False`, `bare.html` ohne, gar kein Rahmen mit `chrome=False`) → `RuntimeError` beim Rendern.
|
|
442
|
+
Beispiel: `/print` in `examples/minimal` (auch im Browser-Smoke).
|
|
443
|
+
- **Protokoll: App-Quelle** (P-107): Logger unter `<name>` ohne eigene `pz.log_source()`-Zuordnung
|
|
444
|
+
landen in der Quelle `<name>` (Anzeigename = `title`) statt in `system`; bildet die App `<name>`
|
|
445
|
+
selbst ab, gilt ihre Zuordnung.
|
|
446
|
+
- `pz.public_mount(pfad)`: eine eingehängte Unter-App (`app.mount`) ohne Anmeldung erreichbar machen.
|
|
447
|
+
- **WebSockets** (FastAPI-`@router.websocket`): Gate und Guard gelten auch dort – `Origin` muss zum
|
|
448
|
+
`Host` passen (fehlender `Origin` = kein Browser, erlaubt), Identität wie bei HTTP
|
|
449
|
+
(`websocket.state.user`), ohne Anmeldung bzw. bei fremdem Origin Schließen mit 1008. `public`
|
|
450
|
+
funktioniert wie bei HTTP-Routen.
|
|
451
|
+
- `HEAD` wird für alle Routen wie `GET` beantwortet (ohne Inhalt), z. B. `HEAD /healthz`.
|
|
452
|
+
- `pillarz.testing.flush_log()`: wartet, bis das Protokoll geschrieben ist (vor dem Lesen von `pz_log`).
|
|
453
|
+
- `route_audit()` prüft WebSocket-Routen (anonym → 1008) und meldet Mounts (außer Static und
|
|
454
|
+
`pz.public_mount`), `Host`- und Starlette-Routen (`Route`/`WebSocketRoute`), an denen der Gate nicht
|
|
455
|
+
läuft. Eingebundene Router werden mit der ganzen Kette ausgewertet.
|
|
456
|
+
|
|
457
|
+
### Changed
|
|
458
|
+
- **`pillarz.testing.configure()` erzwingt ein frisches temporäres `DATA_DIR`** (`<tmp>/pillarz-test-*`,
|
|
459
|
+
ein gesetztes `DATA_DIR` wird überschrieben, `DB_PATH` entfernt) und leert `POSTGRES_*` – außer bei
|
|
460
|
+
`PZ_TEST_PG=1`. Apps, die ihre Tests gegen PostgreSQL fahren, setzen `PZ_TEST_PG=1` und eine
|
|
461
|
+
Datenbank, deren Name `test` als Wortteil trägt (`labelz_test`, `pillarz_test_…`).
|
|
462
|
+
`pillarz.db.reset()` verweigert sonst (SQLite: Datei außerhalb eines Testverzeichnisses).
|
|
463
|
+
Unterprozesse eines Testlaufs übernehmen das Verzeichnis des Elternprozesses.
|
|
464
|
+
- `python -m pillarz migrate --status|--dry-run` legen keine fehlende SQLite-Datei mehr an (Meldung,
|
|
465
|
+
alle Schritte offen); `migrations.status()`/Trockenlauf ohne Datenbank → `MigrationError`.
|
|
466
|
+
- Wartet eine Migration auf SQLite länger als 120 s auf die Dateisperre, gibt es einen
|
|
467
|
+
`MigrationError` mit klarer Meldung statt `sqlite3.OperationalError`.
|
|
468
|
+
- `.secret_key` entsteht bei mehreren gleichzeitig startenden Prozessen genau einmal (Temp-Datei 0600 +
|
|
469
|
+
`os.link`); eine für Gruppe/andere lesbare Schlüsseldatei ergibt eine Warnung.
|
|
470
|
+
- **Protokoll schreibt im Hintergrund:** der Handler ist ein `QueueHandler`, ein eigener Thread schreibt
|
|
471
|
+
die Einträge stapelweise. Loggen blockiert weder den Event-Loop noch eine offene Transaktion
|
|
472
|
+
(vorher auf SQLite 5 s Sperre, verlorener Eintrag und danach „database is locked“; auf PG eine zweite
|
|
473
|
+
Pool-Verbindung). Einträge sind unabhängig von der Transaktion des Aufrufers (bleiben bei Rollback).
|
|
474
|
+
Beim Herunterfahren wird die Warteschlange geleert; Tests lesen `pz_log` nach `testing.flush_log()`.
|
|
475
|
+
- Protokoll-Obergrenze 20 000 Einträge gilt getrennt für `auth`/`admin` und alle übrigen Quellen.
|
|
476
|
+
- `log_once` drosselt nach festen Schlüsseln (Routen-Template statt Pfad) und verdrängt bei Überlauf
|
|
477
|
+
die ältesten Schlüssel, statt alle zu vergessen.
|
|
478
|
+
- HTML-GET mit unerwartetem Fehler bekommt die Fehlerseite (500) statt JSON.
|
|
479
|
+
- Templates: `autoescape` für alle Templates (unabhängig von der Endung).
|
|
480
|
+
- Body-Grenzen gelten für alle Methoden (auch `GET`/`HEAD`/`OPTIONS`); ein `Content-Length` ohne
|
|
481
|
+
gültige Zahl ergibt 411.
|
|
482
|
+
|
|
483
|
+
### Fixed
|
|
484
|
+
- Validatoren (Benutzername, Mail, SQL-Bezeichner, Migrationsnamen, App-/Navigations-/Theme-Kennungen,
|
|
485
|
+
Farben, CSP-Quellen, Sprachcode) ließen ein abschließendes `\n` durch (`re.match` mit `$`) – jetzt
|
|
486
|
+
`fullmatch`.
|
|
487
|
+
- PostgreSQL-Pool: kam nach einer toten Verbindung kein Ersatz (`PoolTimeout`), wurde die tote
|
|
488
|
+
Verbindung ein zweites Mal an den Pool zurückgegeben (bei gesunden Verbindungen ein `ValueError`, der
|
|
489
|
+
den `PoolTimeout` überdeckt).
|
|
490
|
+
- Ersteinrichtung auf SQLite mit mehreren Prozessen: „database is locked“ → `SetupClosed`.
|
|
491
|
+
- `scripts/release.sh`: mit `SKIP_TESTS=1` fehlte der Interpreter für den Build-Schritt.
|
|
492
|
+
|
|
493
|
+
### Security
|
|
494
|
+
- `SECRET_KEY` aus der Umgebung: bekannte Vorlagenwerte (`replace-with-…`, `changeme`, `example` …) und
|
|
495
|
+
Werte mit weniger als 10 verschiedenen Zeichen brechen den Start ab.
|
|
496
|
+
- Passwörter mit einzelnen UTF-16-Surrogaten lehnt `password_problem` ab (nicht kodier-/hashbar).
|
|
497
|
+
- CI: Versionsnummer aus dem Tag erst gegen PEP 440 prüfen und nie in Python-Quelltext einsetzen;
|
|
498
|
+
PyPI-Token (`PYPI_TOKEN`) nur über die Umgebung an `twine`, nie in der Kommandozeile;
|
|
499
|
+
probe-install installiert von pypi.org und vergleicht den Hash dort mit dem im publish-Job
|
|
500
|
+
hochgeladenen Wheel; pip-audit auch über `requirements-dev.txt`; kein Cache-Speichern aus
|
|
501
|
+
Pull-Requests.
|
|
502
|
+
- Review Paket B: Mounts, `Host`- und Starlette-Routen liefen am Gate vorbei – der Guard weist anonyme
|
|
503
|
+
Anfragen an sie jetzt ab (401 bzw. `303 /login`), außer pillarz' Static und `pz.public_mount()`.
|
|
504
|
+
- `public` zählt nur noch als direkte Abhängigkeit der Route bzw. eines Routers der Kette – eine
|
|
505
|
+
Hilfs-Dependency, die intern `Depends(public)` nutzt, macht Routen nicht mehr öffentlich. Umgekehrt
|
|
506
|
+
wird `public` an verschachtelt eingebundenen Routern (`include_router(…, dependencies=[…])`) erkannt.
|
|
507
|
+
- WebSocket-Verbindungen liefen am Guard vorbei (kein Origin-Check, keine Identität).
|
|
508
|
+
- CSP-Ergänzungen: `javascript:` überall abgelehnt; `worker-src`/`child-src` nur `'self'`/`blob:`; Hosts
|
|
509
|
+
bei reinem HTTPS nur `https://` (`connect-src` zusätzlich `wss://`), `ws(s)://` nur in `connect-src`;
|
|
510
|
+
Hashes in `style-src` abgelehnt (sie schalteten `'unsafe-inline'` still ab).
|
|
511
|
+
- Pfade in Logzeilen einzeilig und gekürzt (`oneline(…)[:200]`, keine Log-Injection auf stdout).
|
|
512
|
+
- `PoolTimeout`/`HTTPException` aus der Identitätsauflösung im Guard → 503 mit `Retry-After` bzw. die
|
|
513
|
+
passende Fehlerantwort statt 500.
|
|
514
|
+
- Template-Namensraum: `./pillarz/…` und ähnliche Schreibweisen umgingen den Schutz der
|
|
515
|
+
pillarz-Templates; ein App-Ordner `templates/pillarz/` bricht den Start jetzt ab.
|
|
516
|
+
|
|
517
|
+
### Upgrade
|
|
518
|
+
- **Default deny:** Routen einer App sind ab 0.1-Paket B ohne Anmeldung gesperrt. Öffentliche Routen
|
|
519
|
+
(Webhooks, Startseite ohne Login …) brauchen `Depends(pillarz.web.public)` bzw. `@public_route`;
|
|
520
|
+
`pillarz.testing.route_audit(app)` im Testlauf der App zeigt vergessene Stellen.
|
|
521
|
+
- App-Ordner `static/pillarz/` ist verboten (`create_app()` bricht ab); `static/logo.svg` und
|
|
522
|
+
`static/favicon.svg` der App ersetzen Marke bzw. Favicon von pillarz.
|
|
523
|
+
- App-Ordner `templates/pillarz/` ist ebenso verboten (`create_app()` bricht ab).
|
|
524
|
+
- `render(**ctx)` lehnt von pillarz belegte Kontextschlüssel ab (`user`, `boot`, `nav_items`, `i18n`,
|
|
525
|
+
`version`, `help`, `flash`, `_` … → `ValueError`).
|
|
526
|
+
- Eigene Mounts (`app.mount`) sind anonym gesperrt; öffentliche Unter-Apps mit `pz.public_mount(pfad)`
|
|
527
|
+
freigeben. `app.add_route()` statt FastAPI-Routen meldet `route_audit()`.
|
|
528
|
+
- Tests, die `pz_log` lesen, vorher `pillarz.testing.flush_log()` aufrufen.
|
|
529
|
+
- Eine vorhandene `users`-Tabelle (z. B. labelz 0.12) wird übernommen: `0001_initial` ergänzt fehlende
|
|
530
|
+
Spalten (`role`, `email_confirmed`, `locale`, `region`, `updated_at` …), setzt `email_confirmed = 1`
|
|
531
|
+
für Konten mit Adresse und `pz_meta.setup_done`, wenn es Konten gibt. Datenumbau (`is_admin` →
|
|
532
|
+
`role`, `oidc_links` → `pz_tokens`, `themes` → `pz_themes`, `meta.settings` → `pz_settings`) ist
|
|
533
|
+
Sache einer App-Migration, die nach `0001_initial` läuft (Architektur §5.4).
|
|
534
|
+
- Installation in einer App: normal von pypi.org, exakter Pin mit Hash (`pillarz==X.Y.Z
|
|
535
|
+
--hash=sha256:…` im Lock der App), Installation mit `--require-hashes` – kein zusätzlicher Index, kein
|
|
536
|
+
Token im Image-Build.
|