tinysesam 0.19.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.
Files changed (113) hide show
  1. tinysesam-0.19.0/API.md +561 -0
  2. tinysesam-0.19.0/CHANGELOG.md +2476 -0
  3. tinysesam-0.19.0/KONFIGURATION.md +267 -0
  4. tinysesam-0.19.0/LICENSE +21 -0
  5. tinysesam-0.19.0/MANIFEST.in +43 -0
  6. tinysesam-0.19.0/PKG-INFO +956 -0
  7. tinysesam-0.19.0/README.md +890 -0
  8. tinysesam-0.19.0/SECURITY.md +45 -0
  9. tinysesam-0.19.0/deploy/fail2ban/tinysesam-filter.conf +24 -0
  10. tinysesam-0.19.0/deploy/fail2ban/tinysesam-jail.conf +41 -0
  11. tinysesam-0.19.0/deploy/fail2ban/tinysesam-verify-filter.conf +20 -0
  12. tinysesam-0.19.0/deploy/forward-auth/Caddyfile +81 -0
  13. tinysesam-0.19.0/deploy/forward-auth/docker-compose.yml +100 -0
  14. tinysesam-0.19.0/deploy/forward-auth/nginx-pfad.conf +120 -0
  15. tinysesam-0.19.0/deploy/forward-auth/nginx.conf +62 -0
  16. tinysesam-0.19.0/deploy/forward-auth/traefik.yml +59 -0
  17. tinysesam-0.19.0/deploy/systemd/README.md +43 -0
  18. tinysesam-0.19.0/deploy/systemd/tinysesam-gc.service +18 -0
  19. tinysesam-0.19.0/deploy/systemd/tinysesam-gc.timer +11 -0
  20. tinysesam-0.19.0/examples/demo.py +34 -0
  21. tinysesam-0.19.0/examples/showcase.py +354 -0
  22. tinysesam-0.19.0/i18n/README.de.md +911 -0
  23. tinysesam-0.19.0/i18n/SECURITY.de.md +46 -0
  24. tinysesam-0.19.0/pyproject.toml +91 -0
  25. tinysesam-0.19.0/setup.cfg +4 -0
  26. tinysesam-0.19.0/tests/_kit/KIT_VERSION +1 -0
  27. tinysesam-0.19.0/tests/_kit/MANIFEST.sha256 +15 -0
  28. tinysesam-0.19.0/tests/_kit/__init__.py +0 -0
  29. tinysesam-0.19.0/tests/_kit/backlog.py +188 -0
  30. tinysesam-0.19.0/tests/_kit/headers.py +220 -0
  31. tinysesam-0.19.0/tests/_kit/hygiene.py +903 -0
  32. tinysesam-0.19.0/tests/_kit/hygiene_policy.json +199 -0
  33. tinysesam-0.19.0/tests/_kit/manifest.py +77 -0
  34. tinysesam-0.19.0/tests/_kit/python_matrix.json +25 -0
  35. tinysesam-0.19.0/tests/_kit/report.py +48 -0
  36. tinysesam-0.19.0/tests/api_surface.json +285 -0
  37. tinysesam-0.19.0/tests/e2e_stage.py +336 -0
  38. tinysesam-0.19.0/tests/run_all.py +124 -0
  39. tinysesam-0.19.0/tests/test_account.py +107 -0
  40. tinysesam-0.19.0/tests/test_admin.py +79 -0
  41. tinysesam-0.19.0/tests/test_adminmount.py +76 -0
  42. tinysesam-0.19.0/tests/test_api_surface.py +187 -0
  43. tinysesam-0.19.0/tests/test_apikeys.py +136 -0
  44. tinysesam-0.19.0/tests/test_audit_runde2.py +919 -0
  45. tinysesam-0.19.0/tests/test_authz_hardening.py +189 -0
  46. tinysesam-0.19.0/tests/test_bestandsdaten.py +252 -0
  47. tinysesam-0.19.0/tests/test_browser.py +432 -0
  48. tinysesam-0.19.0/tests/test_chain.py +237 -0
  49. tinysesam-0.19.0/tests/test_cli.py +134 -0
  50. tinysesam-0.19.0/tests/test_cookies.py +246 -0
  51. tinysesam-0.19.0/tests/test_core.py +84 -0
  52. tinysesam-0.19.0/tests/test_csp.py +89 -0
  53. tinysesam-0.19.0/tests/test_csrf.py +111 -0
  54. tinysesam-0.19.0/tests/test_forward_auth.py +321 -0
  55. tinysesam-0.19.0/tests/test_gateway.py +81 -0
  56. tinysesam-0.19.0/tests/test_group_roles.py +77 -0
  57. tinysesam-0.19.0/tests/test_hardening.py +282 -0
  58. tinysesam-0.19.0/tests/test_hardening2.py +66 -0
  59. tinysesam-0.19.0/tests/test_i18n.py +98 -0
  60. tinysesam-0.19.0/tests/test_identifier.py +226 -0
  61. tinysesam-0.19.0/tests/test_kern_install.py +204 -0
  62. tinysesam-0.19.0/tests/test_ldap.py +548 -0
  63. tinysesam-0.19.0/tests/test_magic.py +108 -0
  64. tinysesam-0.19.0/tests/test_matrix.py +95 -0
  65. tinysesam-0.19.0/tests/test_methods.py +184 -0
  66. tinysesam-0.19.0/tests/test_oidc_jwks.py +479 -0
  67. tinysesam-0.19.0/tests/test_packaging.py +311 -0
  68. tinysesam-0.19.0/tests/test_pin.py +96 -0
  69. tinysesam-0.19.0/tests/test_pin_stepup.py +380 -0
  70. tinysesam-0.19.0/tests/test_presets.py +37 -0
  71. tinysesam-0.19.0/tests/test_ratelimit.py +77 -0
  72. tinysesam-0.19.0/tests/test_recovery_reset.py +143 -0
  73. tinysesam-0.19.0/tests/test_register.py +119 -0
  74. tinysesam-0.19.0/tests/test_repo.py +596 -0
  75. tinysesam-0.19.0/tests/test_resource.py +137 -0
  76. tinysesam-0.19.0/tests/test_saml.py +278 -0
  77. tinysesam-0.19.0/tests/test_security_log.py +562 -0
  78. tinysesam-0.19.0/tests/test_sessions_logout.py +77 -0
  79. tinysesam-0.19.0/tests/test_sicherheit_befunde.py +2076 -0
  80. tinysesam-0.19.0/tests/test_site.py +238 -0
  81. tinysesam-0.19.0/tests/test_stepup.py +374 -0
  82. tinysesam-0.19.0/tests/test_theme_errors.py +165 -0
  83. tinysesam-0.19.0/tests/test_typen.py +83 -0
  84. tinysesam-0.19.0/tests/test_views_remember.py +85 -0
  85. tinysesam-0.19.0/tests/voraussetzung.py +47 -0
  86. tinysesam-0.19.0/tinysesam/__init__.py +28 -0
  87. tinysesam-0.19.0/tinysesam/__main__.py +297 -0
  88. tinysesam-0.19.0/tinysesam/admin.py +478 -0
  89. tinysesam-0.19.0/tinysesam/config.py +500 -0
  90. tinysesam-0.19.0/tinysesam/errors.py +67 -0
  91. tinysesam-0.19.0/tinysesam/gateway.py +271 -0
  92. tinysesam-0.19.0/tinysesam/konfigpruefung.py +389 -0
  93. tinysesam-0.19.0/tinysesam/ldap_.py +177 -0
  94. tinysesam-0.19.0/tinysesam/mailer.py +47 -0
  95. tinysesam-0.19.0/tinysesam/manager.py +2819 -0
  96. tinysesam-0.19.0/tinysesam/messages.py +484 -0
  97. tinysesam-0.19.0/tinysesam/oidc.py +649 -0
  98. tinysesam-0.19.0/tinysesam/passwords.py +120 -0
  99. tinysesam-0.19.0/tinysesam/py.typed +0 -0
  100. tinysesam-0.19.0/tinysesam/router.py +994 -0
  101. tinysesam-0.19.0/tinysesam/saml_.py +136 -0
  102. tinysesam-0.19.0/tinysesam/security.py +470 -0
  103. tinysesam-0.19.0/tinysesam/store.py +1023 -0
  104. tinysesam-0.19.0/tinysesam/templates.py +708 -0
  105. tinysesam-0.19.0/tinysesam/theme.py +41 -0
  106. tinysesam-0.19.0/tinysesam/totp.py +62 -0
  107. tinysesam-0.19.0/tinysesam/webauthn_.py +217 -0
  108. tinysesam-0.19.0/tinysesam.egg-info/PKG-INFO +956 -0
  109. tinysesam-0.19.0/tinysesam.egg-info/SOURCES.txt +111 -0
  110. tinysesam-0.19.0/tinysesam.egg-info/dependency_links.txt +1 -0
  111. tinysesam-0.19.0/tinysesam.egg-info/entry_points.txt +2 -0
  112. tinysesam-0.19.0/tinysesam.egg-info/requires.txt +41 -0
  113. tinysesam-0.19.0/tinysesam.egg-info/top_level.txt +1 -0
@@ -0,0 +1,561 @@
1
+ # API — die öffentliche Oberfläche von `TinySesam`
2
+
3
+ <!-- GENERIERT von scripts/_api_doku.py — nicht von Hand pflegen.
4
+ Neu bauen: `python3 scripts/_api_doku.py` -->
5
+
6
+ Diese Namen hält `tests/api_surface.json` fest: Was hier steht, ändert sich nicht ohne eine
7
+ bewusste Entscheidung und einen Eintrag im CHANGELOG.
8
+
9
+ Die READMEs zeigen die **Wege** (welche Methode wofür, wie man sie kombiniert). Hier steht, was
10
+ es überhaupt gibt — die Frage „gibt es dafür schon etwas?" beantwortet diese Seite, nicht der
11
+ Quelltext.
12
+
13
+ > **Gemessen, nicht ausgewählt.** Erfasst ist alles ohne führenden Unterstrich. Welche dieser
14
+ > Methoden auf Dauer öffentlich sein *sollen*, ist eine offene Entscheidung für 1.0
15
+ > ([M-1](backlog/M-1-api-stabil-1-0.md)) — bis dahin gilt: eingefroren ist, was hier steht.
16
+
17
+ Die Konfigurationsfelder stehen in [KONFIGURATION.md](KONFIGURATION.md).
18
+
19
+ ## `TinySesam`
20
+
21
+ ### `add_messages(lang, mapping: 'dict')`
22
+
23
+ Eigene Übersetzungen ergänzen/überschreiben (haben Vorrang vor den eingebauten).
24
+
25
+ ### `admin_claim_token() -> 'Optional[str]'`
26
+
27
+ Weg 2: Einmal-Token. Solange kein Admin existiert, gibt es ein Token, das genau einmal eingelöst werden kann (`/auth/claim-admin?token=…`). Der Wert geht beim Start auf stderr bzw. in `admin_claim_token_file` (0600) — wer den Server betreibt, hat ihn; wer bloß die URL kennt oder das Log lesen kann, nicht (B5-03). Läuft ab.
28
+
29
+ ### `admin_exists() -> 'bool'`
30
+
31
+ Gibt es mindestens einen Admin? Die beiden Bootstrap-Wege greifen nur, solange nicht.
32
+
33
+ ### `admin_router()`
34
+
35
+ Eigenständiger Admin-Router (relative Pfade) — an beliebigem Prefix / Sub-App / Port montierbar, oder (admin_ui_enabled=False) nur die JSON-API fürs eigene Panel.
36
+
37
+ ### `all_security() -> 'dict'`
38
+
39
+ Alle Härtungs-Schwellen als Dict (Vorgaben, überschrieben von dem, was im Panel steht).
40
+
41
+ ### `api_key_art(key) -> 'str'`
42
+
43
+ Die Art eines Keys ("automat"/"mensch") — ohne ihn zu benutzen.
44
+
45
+ ### `apply_factor(request, user_id, factor, ip=None, ua=None, remember=True, email_bestaetigt: 'Optional[bool]' = None) -> 'tuple[str, bool, bool]'`
46
+
47
+ Einen bestätigten Faktor anwenden: an die laufende Sitzung desselben Users anhängen (Ketten-Schritt) ODER eine neue Sitzung starten (Erstfaktor/Identitätswechsel). Gibt (token, session_ok, is_new). Bei is_new muss der Aufrufer set_cookie(resp, token) rufen.
48
+
49
+ ### `apply_idp_groups(user_id, groups, mapping: 'dict', substring: 'Optional[bool]' = None)`
50
+
51
+ IdP-Gruppen → lokale Rollen (beim Login). Ziel '__admin__' setzt das Admin-Flag (nur grant, nie automatisch entziehen). Gemappte Rollen werden synchronisiert (bei Wegfall der Gruppe entfernt), manuell vergebene Rollen bleiben.
52
+
53
+ ### `audit(event, username=None, ip=None, detail=None)`
54
+
55
+ Einen Vorgang ins Audit-Log schreiben. `detail` nimmt alles, was später die Frage „warum" beantwortet.
56
+
57
+ ### `check_ldap(username, password) -> 'Optional[dict]'`
58
+
59
+ Passwort gegen LDAP prüfen. Bei Erfolg lokalen User finden/anlegen und zurückgeben. Zählt wie ein Passwort-Login (Faktor 'password').
60
+
61
+ ### `check_password(username, password) -> 'Optional[dict]'`
62
+
63
+ Benutzername/E-Mail + Passwort prüfen. Gibt das Konto zurück oder None — und braucht bei beiden Ausgängen gleich lange (keine Konto-Erkundung).
64
+
65
+ ### `check_pin(username, pin) -> 'Optional[dict]'`
66
+
67
+ Wie `check_password`, nur mit der persönlichen PIN.
68
+
69
+ ### `check_resource(name, secret) -> 'bool'`
70
+
71
+ Das Geheimnis einer gesperrten Ressource prüfen (ohne sie freizuschalten — das tut `unlock_resource`).
72
+
73
+ ### `check_saml(nameid, attrs) -> 'Optional[dict]'`
74
+
75
+ Aus einer geprüften SAML-Assertion einen lokalen User finden/anlegen. Faktor 'saml'.
76
+
77
+ ### `client_ip(request: 'Request') -> 'str'`
78
+
79
+ Die echte Client-IP. Hinter einem Proxy nur dann aus `X-Forwarded-For`, wenn der Peer in `trusted_proxies` steht — sonst wäre der Header fälschbar.
80
+
81
+ ### `complete_mfa(token)`
82
+
83
+ Historischer Name für `complete_totp()` — bleibt erhalten, damit nichts bricht.
84
+
85
+ ### `complete_totp(token) -> 'Optional[str]'`
86
+
87
+ Den TOTP-Schritt abschließen: Faktor `totp` an die laufende Sitzung anhängen.
88
+
89
+ ### `consume_admin_claim(token, user) -> 'bool'`
90
+
91
+ Das Einmal-Token einlösen und dieses Konto zum Admin machen. Gilt genau einmal.
92
+
93
+ ### `create_api_key(user_id, name=None, expires_days=None, roles=None, kind: 'str' = 'automat') -> 'dict'`
94
+
95
+ Neuen API-Key erzeugen. Rückgabe enthält 'key' im KLARTEXT — nur EINMAL (danach nur der Hash).
96
+
97
+ ### `create_invite(email, base_url, roles=None, is_admin=False, ttl_min=None) -> 'dict'`
98
+
99
+ Einladung erzeugen (+ optional versenden). Rückgabe {url, token}. Der Token trägt die vorgesehenen Rollen/Adminrechte; eingelöst wird er erst bei der Registrierung. `base_url` wird geprüft (`ConfigError` bei einem fremden Host, siehe `magic_url`).
100
+
101
+ ### `create_magic_token(purpose, user_id=None, email=None, ttl_min=None, payload=None) -> 'str'`
102
+
103
+ Einmal-Token erzeugen (Klartext-Rückgabe). Nur der sha256-Hash liegt in der DB.
104
+
105
+ ### `create_service(username, roles=None, display_name=None) -> 'int'`
106
+
107
+ Service-/Daemon-Account: kein interaktiver Login, nur API-Keys. Rollen = Rechte-Scope.
108
+
109
+ ### `create_user(username, password=None, is_admin=False, roles=None, display_name=None, email=None, is_service=False, email_verified: 'bool' = True) -> 'int'`
110
+
111
+ Ein Konto anlegen und seine ID zurückgeben. `is_service=True` für Maschinen: kein Login, nur API-Keys. Eine bereits vergebene Kennung wirft `ConfigError` — **neu auch beim doppelten Benutzernamen**, der bis 0.18.x als `sqlite3.IntegrityError` aus der Datenbank kam (`e.feld`/`e.besitzer_id` sagen, was kollidierte).
112
+
113
+ ### `csrf_rotieren(response) -> 'str'`
114
+
115
+ Ein frisches CSRF-Token setzen — beim Login.
116
+
117
+ ### `csrf_token(request: 'Optional[Request]' = None) -> 'str'`
118
+
119
+ Das CSRF-Token dieses Browsers — vorhandenes Cookie wiederverwenden, sonst neu würfeln.
120
+
121
+ ### `current_user(request) -> 'Optional[dict]'`
122
+
123
+ Das angemeldete Konto zu diesem Request — aus der Sitzung ODER einem API-Key. None, wenn niemand angemeldet ist.
124
+
125
+ ### `darf_mfa_einrichten(user_id: 'int', jetzt: 'Optional[int]' = None) -> 'bool'`
126
+
127
+ Darf dieses Konto den von der Kette verlangten Faktor **selbst** einrichten? (R3-1)
128
+
129
+ ### `disable_pin(user_id)`
130
+
131
+ Die PIN eines Kontos entfernen (wird protokolliert — ein zweiter Faktor verschwindet nicht unbemerkt).
132
+
133
+ ### `ensure_admin(username, password) -> 'bool'`
134
+
135
+ Bootstrap: legt einen Admin an, WENN noch kein User existiert. True bei Anlage.
136
+
137
+ ### `factor_entry(step, nxt='/') -> 'str'`
138
+
139
+ Die Adresse der Eingabeseite für einen Faktor-Schritt, mit `next` daran.
140
+
141
+ ### `find_user(identifier) -> 'Optional[dict]'`
142
+
143
+ Konto zur Login-Kennung suchen — je nach `config.login_identifier`.
144
+
145
+ ### `forward_login_url(orig_url: 'str', request: 'Optional[Request]' = None) -> 'str'`
146
+
147
+ Zentrale Login-URL (auf base_url bzw. abgeleitet) mit next=<orig_url>.
148
+
149
+ ### `forward_response_headers(user) -> 'dict'`
150
+
151
+ Die Header, die der Proxy bei einer erfolgreichen Prüfung an die App weiterreicht.
152
+
153
+ ### `forwarded_url(request: 'Request') -> 'str'`
154
+
155
+ Ursprüngliche vom Proxy angefragte URL rekonstruieren (Caddy/Traefik: X-Forwarded-*, nginx: X-Original-URL). Fallback: Referer bzw. '/'.
156
+
157
+ ### `gc(attempts_older_than_sec: 'int' = 86400) -> 'dict'`
158
+
159
+ Aufräumen: abgelaufene Sessions/Flows/Magic-Tokens/Ressourcen-Unlocks + alte Login-Versuche. Regelmäßig aufrufen (Cron/Startup/Scheduler) — sonst wachsen die Tabellen. Das Audit-Log bleibt (bewusst) unangetastet. Gibt Anzahl gelöschter Zeilen je Bereich.
160
+
161
+ ### `generate_recovery_codes(user_id, n=None) -> 'list'`
162
+
163
+ Neue Einmal-Codes erzeugen (ersetzt vorhandene). Klartext-Rückgabe NUR EINMAL.
164
+
165
+ ### `get_user(user_id) -> 'Optional[dict]'`
166
+
167
+ Ein Konto per ID lesen, oder None.
168
+
169
+ ### `grant_mfa_enrollment(user_id: 'int', minutes: 'int' = 60) -> 'int'`
170
+
171
+ Ein Einrichtungsfenster öffnen und seinen Ablauf zurückgeben.
172
+
173
+ ### `has_pin(user_id) -> 'bool'`
174
+
175
+ Hat dieses Konto eine PIN eingerichtet?
176
+
177
+ ### `has_role(user, role, admin_implies=None) -> 'bool'`
178
+
179
+ Hat der User die Rolle? Ein Admin erfüllt standardmäßig JEDE Rolle (`config.admin_implies_roles`). Wer Rechte allein an IdP-Gruppen hängt, schaltet das ab — sonst ist jeder lokale Admin automatisch auch „editor", „viewer", … .
180
+
181
+ ### `install_error_pages(app)`
182
+
183
+ Themed Fehlerseiten registrieren (opt-in). Browser bekommen die 'error'-Seite (im Branding), API-Clients JSON; Redirects (Login/Reauth/Faktor, via Location-Header) bleiben Redirects.
184
+
185
+ ### `install_https(app)`
186
+
187
+ HTTPS gemäß config.https_mode: 'force' → HTTP→HTTPS-Redirect-Middleware; 'warn'/'off' → läuft auch OHNE Zertifikat (bei 'warn' Panel-Hinweis). Gibt den Modus zurück.
188
+
189
+ ### `is_admin(user) -> 'bool'`
190
+
191
+ Ist dieses Konto Admin? Nimmt eine Kontozeile, kein Request.
192
+
193
+ ### `is_locked(username, ip) -> 'bool'`
194
+
195
+ Zu viele Fehlversuche im Fenster — pro User ODER pro IP (IP-Schwelle höher wg. NAT).
196
+
197
+ ### `is_password_change_locked(username, ip) -> 'bool'`
198
+
199
+ Eigener, methoden-scoped Lockout für die Alt-Passwort-Abfrage der Kontoseite.
200
+
201
+ ### `is_pin_locked(username, ip) -> 'bool'`
202
+
203
+ Eigener, methoden-scoped Lockout für PIN (kurzer Keyspace). Zusätzlich zu is_locked().
204
+
205
+ ### `is_reauth_locked(username, ip) -> 'bool'`
206
+
207
+ Eigener, methoden-scoped Lockout für die Step-up-Bestätigung (`/auth/reauth`).
208
+
209
+ ### `is_resource_locked(username, ip) -> 'bool'`
210
+
211
+ Eigener, methoden-scoped Lockout für die Bereichs-PIN (`/auth/resource/…`).
212
+
213
+ ### `is_secure(request: 'Request') -> 'bool'`
214
+
215
+ HTTPS aktiv? (direkt, via X-Forwarded-Proto hinter Proxy, oder localhost).
216
+
217
+ ### `issue_csrf(response: 'Response') -> 'str'`
218
+
219
+ CSRF-Token erzeugen und als Cookie setzen — für eigene Templates (Jinja & Co.), die nicht über `render_page()` laufen. Rückgabe gehört ins Formularfeld `_csrf` bzw. den Header `X-CSRF-Token`. Ist CSRF abgeschaltet, passiert nichts und der Rückgabewert ist leer.
220
+
221
+ ### `json_body(request: 'Request') -> 'dict'`
222
+
223
+ JSON-Body robust lesen: ungültiger/leerer Body → 400 statt 500. Erzwingt CSRF (Header X-CSRF-Token) für cookie-basierte Clients; API-Key-Requests sind ausgenommen.
224
+
225
+ ### `kennung_vergeben(kennung, exclude_id=None) -> 'Optional[dict]'`
226
+
227
+ Gehört diese Login-Kennung schon einem Konto — in IRGENDEINEM der beiden Namensräume?
228
+
229
+ ### `list_api_keys(user_id)`
230
+
231
+ Die API-Keys eines Kontos — ohne die Schlüssel selbst, die gibt es nur einmal bei der Ausgabe.
232
+
233
+ ### `list_resource_secrets()`
234
+
235
+ Alle gesperrten Ressourcen (Namen und Beschreibungen, keine Geheimnisse).
236
+
237
+ ### `login_fresh(request: 'Request', user: 'Optional[dict]' = None) -> 'bool'`
238
+
239
+ True, wenn die **Anmeldung** höchstens `stepup_max_age_sec` zurückliegt.
240
+
241
+ ### `login_redirect_after(request, token, user_id, nxt)`
242
+
243
+ Zielredirect nach einem Faktor: nxt wenn Sitzung komplett, sonst Eingabeseite des nächsten Faktors.
244
+
245
+ ### `logout(request, response)`
246
+
247
+ Die Sitzung dieses Requests beenden und das Cookie löschen.
248
+
249
+ ### `magic_url(raw, base_url, purpose='login') -> 'str'`
250
+
251
+ Der Link, den der Empfänger anklickt — Pfad je nach Zweck (`TOKEN_PATHS`). `base_url` wird geprüft: ein fremder Host wirft `ConfigError` — in einer Route liefert `public_base(request)` die geprüfte Basis.
252
+
253
+ ### `mail_configured() -> 'bool'`
254
+
255
+ Kann überhaupt eine Mail hinausgehen — per SMTP oder per `set_mailer`?
256
+
257
+ ### `maybe_promote_admin(user, email_bestaetigt: 'Optional[bool]' = None, faktor: 'Optional[str]' = None) -> 'bool'`
258
+
259
+ Weg 1: Allowlist. Wer in `admin_identifiers` steht, wird beim Login Admin — egal über welche Methode (auch OIDC/SAML/LDAP); eine Allowlist-ADRESSE aber nur mit einem Beleg, dass sie dem Anmeldenden gehört, und über SAML/LDAP gibt es keinen. Danach nie wieder.
260
+
261
+ ### `mfa_pending(user_id) -> 'bool'`
262
+
263
+ TOTP verlangt? Ja, wenn ein bestätigtes TOTP für dieses Konto existiert.
264
+
265
+ ### `next_login_step(user_id, done)`
266
+
267
+ Nächster offener Faktor bis zur vollen (globalen) Anmeldung, oder None wenn fertig.
268
+
269
+ ### `oidc_anwendung(url_oder_host: 'str') -> 'str'`
270
+
271
+ Der Client-Schlüssel für diese Adresse — "" wenn diese Installation nur eine Anwendung schützt. Der leere Rückgabewert ist Absicht: Er hält jede Aufrufstelle wortgleich beim Verhalten von 0.18.0, solange `oidc_clients` leer ist.
272
+
273
+ ### `oidc_freigabe_gueltig(token_hash: 'str', client: 'str') -> 'tuple'`
274
+
275
+ Darf diese Sitzung in diese Anwendung? Rückgabe `(ja, grund)`.
276
+
277
+ ### `peek_magic(raw, purpose=None) -> 'Optional[dict]'`
278
+
279
+ Token prüfen OHNE ihn zu verbrauchen (für den Invite-Flow: erst bei Registrierung einlösen).
280
+
281
+ ### `pending_user(request) -> 'Optional[dict]'`
282
+
283
+ User einer Session, die noch im MFA-Schritt hängt (mfa_ok=0).
284
+
285
+ ### `public_base(request: 'Optional[Request]' = None, kandidat: 'str' = '') -> 'str'`
286
+
287
+ Die öffentliche Basis-URL für alles, was das Haus verlässt — Mail-Links, Redirect-URIs, SAML-Metadaten. Leer heißt: es gibt keine, der Aufrufer bricht ab.
288
+
289
+ ### `purge_demo() -> 'int'`
290
+
291
+ Die von `seed_demo` angelegten Konten wieder entfernen — genau die, keine gleichnamigen.
292
+
293
+ ### `rate_ok(ip, login: 'bool' = True) -> 'bool'`
294
+
295
+ Darf diese IP noch? Ein Nein schreibt eine Zeile ins Sicherheits-Log (fail2ban liest mit).
296
+
297
+ ### `record_login(username, ip, success, method)`
298
+
299
+ Einen Anmeldeversuch verbuchen. Ein Erfolg räumt nur die Fehlversuche DERSELBEN Methode weg.
300
+
301
+ ### `recovery_codes_remaining(user_id) -> 'int'`
302
+
303
+ Wie viele Einmal-Codes dieses Konto noch hat.
304
+
305
+ ### `redeem_magic(raw, purpose=None) -> 'Optional[dict]'`
306
+
307
+ Token einlösen (one-shot). Gibt {purpose,user_id,email,payload} oder None (ungültig/abgelaufen/benutzt).
308
+
309
+ ### `remove_resource_secret(name)`
310
+
311
+ Eine gesperrte Ressource wieder freigeben (die Sperre entfernen, nicht entsperren).
312
+
313
+ ### `render_page(template, status=200, request: 'Optional[Request]' = None, **ctx) -> 'Response'`
314
+
315
+ `request` mitgeben, wo es eins gibt: dann bleibt ein bereits gesetztes CSRF-Token gültig. Ohne `request` entsteht ein neues — das überschreibt das Cookie und macht *andere* offene Formulare ungültig (klassische „Formular abgelaufen"-Falle).
316
+
317
+ ### `require(mfa: 'bool' = False, admin: 'bool' = False, role=None, factors: 'Optional[list]' = None, strict: 'Optional[bool]' = None, admin_implies: 'Optional[bool]' = None)`
318
+
319
+ Allgemeine Guard-Factory für beliebige Kombinationen — der „Flag am Guard"-Weg: `Depends(auth.require(mfa=True))`, `Depends(auth.require(admin=True, mfa=True))`. `role=` nimmt eine Rolle oder mehrere (`role=["redaktion", "lektorat"]` → eine genügt). factors=[...] verlangt eine bestimmte Faktor-Kette für diese Route (überschreibt die globale), strict=True/False steuert die Reihenfolge: `Depends(auth.require(factors=['oidc','password']))`.
320
+
321
+ ### `require_admin(request: 'Request') -> 'dict'`
322
+
323
+ FastAPI-Dependency (direkt): eingeloggt + Admin (+ Step-up, wenn admin_require_mfa).
324
+
325
+ ### `require_csrf(request: 'Request', submitted)`
326
+
327
+ Für Formular-POSTs: wirft 403, wenn der CSRF-Token fehlt/nicht passt.
328
+
329
+ ### `require_mfa(request: 'Request') -> 'dict'`
330
+
331
+ FastAPI-Dependency (direkt): eingeloggt + frische Step-up-Bestätigung.
332
+
333
+ ### `require_public_base(request: 'Optional[Request]' = None, kandidat: 'str' = '') -> 'str'`
334
+
335
+ Wie `public_base()`, nur ohne Rückweg: keine geprüfte Basis → `ConfigError`.
336
+
337
+ ### `require_resource(name: 'str')`
338
+
339
+ FastAPI-Dependency-Factory: Bereich erst nach Eingabe des Ressourcen-Geheimnisses zugänglich. Unabhängig vom Benutzer-Login. `Depends(auth.require_resource('fotos'))`.
340
+
341
+ ### `require_role(*roles, mfa: 'bool' = False, admin_implies: 'Optional[bool]' = None)`
342
+
343
+ FastAPI-Dependency-Factory: eingeloggt + Rolle. `Depends(auth.require_role('editor'))`.
344
+
345
+ ### `require_session(request: 'Request', user: 'Optional[dict]' = None) -> 'dict'`
346
+
347
+ Eingeloggt — und zwar **interaktiv**: eine Sitzung ja, ein API-Key nein (403).
348
+
349
+ ### `require_user(request: 'Request') -> 'dict'`
350
+
351
+ FastAPI-Dependency (direkt): erzwingt eingeloggten (inkl. MFA) User. Wer keine Rollen braucht: `Depends(auth.require_user)` genügt.
352
+
353
+ ### `resource_unlocked(request: 'Request', name) -> 'bool'`
354
+
355
+ Ist diese Ressource für diesen Browser gerade freigeschaltet?
356
+
357
+ ### `revoke_api_key(key_id, user_id=None)`
358
+
359
+ Einen Key entwerten. Er bleibt in der Liste stehen — wer ihn ausgestellt hat, soll das sehen.
360
+
361
+ ### `revoke_mfa_enrollment(user_id: 'int') -> 'None'`
362
+
363
+ Ein offenes Einrichtungsfenster sofort schliessen.
364
+
365
+ ### `router()`
366
+
367
+ Der FastAPI-Router mit allen aktivierten Routen. Einmal einbinden, fertig.
368
+
369
+ ### `safe_next(next_: 'str') -> 'str'`
370
+
371
+ ?next=-Ziel gegen Open-Redirect absichern (nur relative Pfade bzw. trusted_redirect_hosts).
372
+
373
+ ### `sec(key) -> 'int'`
374
+
375
+ Härtungs-Wert: Store-Setting (Panel) ODER Default.
376
+
377
+ ### `seed_demo() -> 'None'`
378
+
379
+ Beispielkonten anlegen (idempotent). Verlangt `demo_mode=True`.
380
+
381
+ ### `send_login_link(email, base_url, next='/') -> 'bool'`
382
+
383
+ Login-Link an eine E-Mail schicken, WENN ein passender interaktiver User existiert. Rückgabe nur intern — nach außen immer dieselbe Meldung (keine User-Enumeration). `base_url` wird geprüft (`ConfigError` bei einem fremden Host, siehe `magic_url`).
384
+
385
+ ### `send_mail(to, subject, text, html=None)`
386
+
387
+ Eine Mail versenden — über SMTP oder den per `set_mailer` gesetzten Weg.
388
+
389
+ ### `send_password_reset(email, base_url) -> 'bool'`
390
+
391
+ Reset-Link an eine E-Mail schicken, WENN ein passender User existiert. Nach außen immer gleiche Meldung (keine Enumeration). `base_url` wird geprüft (`ConfigError` bei einem fremden Host, siehe `magic_url`).
392
+
393
+ ### `send_verify_email(user_id, email, base_url) -> 'bool'`
394
+
395
+ Den Bestätigungslink für eine Adresse verschicken. False, wenn kein Mailer da ist. `base_url` wird geprüft (`ConfigError` bei einem fremden Host, siehe `magic_url`).
396
+
397
+ ### `session_from_request(request)`
398
+
399
+ Die Sitzungszeile zu diesem Request, oder None. `row["token_hash"]` ist ihr Handle.
400
+
401
+ ### `set_cookie(response, token, remember: 'bool' = True)`
402
+
403
+ Session-Cookie setzen. remember=True → persistentes Cookie (max_age = lange TTL); remember=False → reines Session-Cookie (max_age=None, endet beim Browser-Schließen).
404
+
405
+ ### `set_mailer(fn)`
406
+
407
+ Eigenen Mail-Versand einhängen: fn(to, subject, text, html=None). Überschreibt SMTP.
408
+
409
+ ### `set_password(user_id, password)`
410
+
411
+ Das Passwort eines Kontos setzen (ohne das alte zu prüfen — das ist Sache des Aufrufers).
412
+
413
+ ### `set_pin(user_id, pin)`
414
+
415
+ PIN setzen/ändern. Mindestlänge aus cfg.pin_min_length.
416
+
417
+ ### `set_rate_limiter(limiter)`
418
+
419
+ Eigenes Rate-Limit-Backend einhängen — beliebiges Objekt mit allow(key, max, window)->bool.
420
+
421
+ ### `set_resource_secret(name, secret, kind='pin', label=None)`
422
+
423
+ Geheimnis für einen Bereich setzen/ändern. kind='pin' (numerisch) \| 'password' (Passphrase).
424
+
425
+ ### `set_roles(user_id, roles)`
426
+
427
+ Die Rollen eines Kontos ersetzen.
428
+
429
+ ### `set_security(key, value)`
430
+
431
+ Eine Härtungs-Schwelle zur Laufzeit setzen; sie überlebt den Neustart in der Datenbank.
432
+
433
+ ### `set_template(name, fn)`
434
+
435
+ Eine eingebaute Seite durch einen eigenen Renderer ersetzen: fn(auth, ctx) -> str \| Response.
436
+
437
+ ### `start_session(user_id, method, ip=None, ua=None, remember: 'bool' = True) -> 'tuple[str, bool]'`
438
+
439
+ Neue Session mit dem ersten Faktor. Gibt (token, session_ok). session_ok=False → weitere Schritte nötig.
440
+
441
+ ### `stepup_fresh(request: 'Request', user: 'Optional[dict]' = None) -> 'bool'`
442
+
443
+ True, wenn die aktuelle Sitzung frisch einen Faktor bestätigt hat (Sudo-Frische).
444
+
445
+ ### `stepup_options(user) -> 'list[str]'`
446
+
447
+ Womit kann DIESER User eine Step-up-Bestätigung leisten? Reihenfolge = Vorschlag.
448
+
449
+ ### `t(key, **fmt) -> 'str'`
450
+
451
+ Übersetzten Text für key in config.lang (Fallback en → key). Platzhalter via {name}.
452
+
453
+ ### `totp_begin(user_id)`
454
+
455
+ Die Einrichtung starten: liefert Geheimnis und `otpauth://`-Adresse für den Authenticator — und wirft neu `StateError` (kein `ConfigError`, kein stiller Erfolg), wenn das Konto bereits ein bestätigtes TOTP hat.
456
+
457
+ ### `totp_confirm(user_id, code) -> 'bool'`
458
+
459
+ Die Einrichtung abschliessen — erst mit einem gültigen Code ist TOTP wirklich an.
460
+
461
+ ### `totp_disable(user_id)`
462
+
463
+ TOTP entfernen, samt der Recovery-Codes (beides wird protokolliert).
464
+
465
+ ### `totp_enrollment_user(request) -> 'Optional[dict]'`
466
+
467
+ Wer darf TOTP einrichten, **ohne** schon voll angemeldet zu sein? Sonst None.
468
+
469
+ ### `unlock_resource(request: 'Request', response, name)`
470
+
471
+ Eine Ressource für diesen Browser freischalten und das Cookie setzen.
472
+
473
+ ### `user_roles(user) -> 'list'`
474
+
475
+ Die Rollen eines Kontos als Liste.
476
+
477
+ ### `verify_api_key(key)`
478
+
479
+ (user, key_roles\|None) bei gültigem Key, sonst (None, None).
480
+
481
+ ### `verify_csrf(request: 'Request', submitted) -> 'bool'`
482
+
483
+ Passt das mitgeschickte CSRF-Token zum Cookie? Vergleich in konstanter Zeit.
484
+
485
+ ### `verify_recovery_code(user_id, code) -> 'bool'`
486
+
487
+ Einen Einmal-Code prüfen und verbrauchen. Ein Code gilt genau einmal.
488
+
489
+ ### `verify_totp(user_id, code) -> 'bool'`
490
+
491
+ Einen TOTP-Code prüfen — und ihn dabei verbrauchen.
492
+
493
+ ### `verify_user_password(user_id, password) -> 'bool'`
494
+
495
+ Das Passwort eines BEKANNTEN Kontos prüfen (Step-up: die Identität steht schon fest).
496
+
497
+ ### `verify_user_pin(user_id, pin) -> 'bool'`
498
+
499
+ Wie `verify_user_password`, nur mit der PIN.
500
+
501
+ ### `vermerke_oidc_freigabe(token: 'str', client: 'str', rollen=None) -> 'None'`
502
+
503
+ Der Provider hat für diese Anwendung zugestimmt — an der Sitzung vermerken.
504
+
505
+ ### `version() -> 'str'`
506
+
507
+ Die laufende Version — fürs Panel. TinySesam aktualisiert sich nicht selbst; das erledigt, wer es installiert hat (gepinnter Tag / Wheel eines Releases).
508
+
509
+ ## `TinySesamConfig` — Presets
510
+
511
+ ### `TinySesamConfig.active_directory(ldap_url, upn_suffix=None, base_dn=None, bind_dn='', bind_password='', allowed_groups=None, **overrides)`
512
+
513
+ Preset: Passwort-Login gegen **Active Directory** (via LDAP). Entweder Direkt-Bind per UPN (`upn_suffix="corp.example.com"` → user@corp.example.com) ODER Search-then-Bind über sAMAccountName (`bind_dn`/`bind_password`/`base_dn`). Restliche Felder via **overrides (db_path …).
514
+
515
+ ### `TinySesamConfig.enabled_methods() -> 'list[str]'`
516
+
517
+ Erstfaktoren, die die Login-Seite anbietet. Eine PIN mit `pin_login=False` steht hier bewusst NICHT — sie bleibt als Zusatzfaktor/Step-up nutzbar.
518
+
519
+ ### `TinySesamConfig.entra_id(tenant_id, client_id, client_secret, oidc_name='Microsoft', **overrides)`
520
+
521
+ Preset: **Entra ID / Azure AD** via OIDC (Cloud-AD). tenant_id = Verzeichnis-(Tenant-)ID.
522
+
523
+ ### `TinySesamConfig.local_accounts(**overrides)`
524
+
525
+ Preset: **nur Benutzername + Passwort**, ganz ohne E-Mail.
526
+
527
+ ### `TinySesamConfig.oidc_gateway(issuer, client_id, client_secret, base_url, cookie_domain='', trusted_redirect_hosts=None, allowed_groups=None, group_claim='groups', oidc_name='SSO', oidc_scopes='openid profile email', db_path='tinysesam-gateway.db', https_mode='warn', session_ttl_hours=168, trusted_proxies=None, clients=None, revalidate_minutes=60, **overrides)`
528
+
529
+ Preset: TinySesam als reines **OIDC-Forward-Auth-Gateway** (Authelia-/oauth2-proxy-Stil). Alle anderen Methoden/Features aus, OIDC + Forward-Auth an. Läuft mit `pip install 'tinysesam[oidc]'`. Einzelne Felder via **overrides überschreibbar.
530
+
531
+ ### `TinySesamConfig.pruefen() -> 'list[str]'`
532
+
533
+ Die Konfiguration erneut prüfen — für den Fall, dass sie nach dem Aufbau geändert wurde.
534
+
535
+ ## Fehlertypen
536
+
537
+ Exportiert aus `tinysesam`. Jeder erbt zusätzlich von dem eingebauten Typ, den er ersetzt — bestehendes `except ValueError` / `except RuntimeError` fängt weiter, es wird nur unterscheidbar. **Auf den Meldungstext prüft niemand:** er ist übersetzt und darf sich ändern; die Typen hier und die Attribute an ihnen sind die Zusage.
538
+
539
+ ### `TinySesamError` (erbt von `Exception`)
540
+
541
+ Basis aller eigenen Fehler — `except TinySesamError` fängt alles von hier.
542
+
543
+ ### `ConfigError` (erbt von `TinySesamError`, `ValueError`)
544
+
545
+ Die Konfiguration widerspricht sich oder verspricht etwas, das so nicht wirkt.
546
+
547
+ ### `MailNotConfigured` (erbt von `TinySesamError`, `RuntimeError`)
548
+
549
+ Es sollte eine Mail raus, aber kein Mailer ist eingerichtet.
550
+
551
+ ### `MissingExtra` (erbt von `TinySesamError`, `RuntimeError`)
552
+
553
+ Ein aktivierter Schalter braucht ein Extra, das nicht installiert ist.
554
+
555
+ ### `StateError` (erbt von `TinySesamError`, `RuntimeError`)
556
+
557
+ Der Vorgang passt nicht zum Zustand des Kontos — und wird deshalb verweigert.
558
+
559
+ ---
560
+
561
+ 122 Methoden, 6 Presets, 5 Fehlertypen — erzeugt aus den Docstrings.