# TinySesam Forward-Auth mit Caddy.
#
# WICHTIG (Gotcha): Caddys `forward_auth`-Shortcut reicht eine 401 nur durch und leitet NICHT
# automatisch zum Login um. Für den Redirect braucht es die expandierte reverse_proxy-Form mit
# handle_response, die den vom Auth-Dienst gesetzten Header X-TinySesam-Location auswertet.
#
# Upstreams kommen aus der Umgebung, damit dieselbe Datei auf dem Blech und im Compose stimmt:
# ohne gesetzte Variablen gilt 127.0.0.1 (Dienste auf demselben Host), `docker-compose.yml`
# setzt die Service-Namen. Im Caddy-CONTAINER ist 127.0.0.1 der Container selbst — dort lauscht
# nichts auf 8000, und ein `docker compose up -d` ergäbe einen Stack, der nicht funktioniert.
#
# Voraussetzung in TinySesamConfig:
#   forward_auth_enabled=True
#   base_url="https://auth.example.com"          # zentrale Login-Domain
#   cookie_domain=".example.com"                 # Cookie gilt für alle Subdomains (SSO)
#   trusted_redirect_hosts=["app.example.com"]   # erlaubt next= zurück auf die App
#   trusted_proxies=["<Caddy-IP>/32"]            # echte Client-IP hinter dem Proxy

# Zentraler Auth-Dienst (TinySesam) — eigene Subdomain
auth.example.com {
	reverse_proxy {$TS_AUTH_UPSTREAM:127.0.0.1:8000}
}

# Rollen verlangen (optional): dem Sub-Request `header_up X-TinySesam-Roles redaktion,admin`
# mitgeben — eine der genannten Rollen genügt. TinySesam antwortet dann 403 statt 200, wenn
# jemand angemeldet ist, aber die Rolle fehlt; ohne die Angabe bleibt /auth/forward binär.
# Der Header gehört in DIESE Datei: `header_up` überschreibt einen gleichnamigen Header des
# Clients. (Selbst wenn nicht, käme ein Client damit nicht weiter — mehrere Angaben werden
# UND-verknüpft, er könnte die Prüfung also nur verschärfen.)

# Geschützte App (kann eine beliebige Fremd-App / statische Site sein)
app.example.com {
	route {
		reverse_proxy {$TS_AUTH_UPSTREAM:127.0.0.1:8000} {
			method GET
			# Der Sub-Request geht an /auth/forward — als `rewrite` IM Block, nicht als Pfad vor
			# dem Upstream. Dort stand bis 2026-09-21 `reverse_proxy /auth/forward <upstream>`:
			# Ein führender Pfad ist in Caddy ein MATCHER, kein Rewrite. Die Prüfung lief damit
			# nur für Anfragen, deren eigener Pfad /auth/forward lautet — jeder andere Pfad fiel
			# direkt auf die App durch. Die Vorlage schützte nichts und sah dabei aus wie SSO.
			# (Form wie in Caddys eigener Expansion von `forward_auth`.)
			rewrite /auth/forward
			# Methode und Pfad der ECHTEN Anfrage — der rewrite oben hat beide überschrieben.
			# X-Forwarded-Host/-Proto stehen hier bewusst nicht: die setzt Caddy von sich aus,
			# und `caddy validate` weist die Doppelung als unnötig aus.
			header_up X-Forwarded-Method {method}
			header_up X-Forwarded-Uri {uri}
			# header_up X-TinySesam-Roles redaktion,admin   # nur mit einer dieser Rollen durch
			# nur die Auth-Antwort-Header übernehmen
			# Andere Namen (z.B. X-WEBAUTH-USER für Grafana): TinySesamConfig(forward_headers={...})
			# setzen und sie hier mitziehen — übernommen wird nur, was hier steht.
			#
			# Header-Form in `{rp.header.…}` (H-16): Caddy sucht den Namen Zeichen für Zeichen
			# in der Form, die Gos textproto daraus macht — jedes Wort gross, der Rest klein
			# (`Remote-User`, `X-Tinysesam-Location`). Eine andere Schreibweise ergibt einen
			# LEEREN Wert, ohne Fehler und ohne Logzeile. tests/test_bestandsdaten.py prüft jeden
			# Platzhalter dieser Datei gegen genau diese Form.
			@ok status 200
			handle_response @ok {
				request_header Remote-User {rp.header.Remote-User}
				request_header Remote-Groups {rp.header.Remote-Groups}
				request_header Remote-Email {rp.header.Remote-Email}
				request_header Remote-Name {rp.header.Remote-Name}
			}
			# nicht eingeloggt → Redirect zur Login-URL aus dem Header
			#
			# `X-Tinysesam-Location` mit kleinem s: Caddy legt die Antwort-Header unter dem
			# Namen ab, den Gos HTTP-Client daraus macht (textproto-Kanonisierung — jedes Wort
			# gross, der Rest klein), und der Platzhalter wird Zeichen für Zeichen gesucht. Mit
			# der Repo-Schreibweise X-TinySesam-Location traf er nichts: eine 302 mit LEEREM
			# Location-Header, der Browser blieb stehen statt zum Login zu gehen. Auf der
			# Leitung ist der Name weiterhin case-insensitiv — nur dieser Platzhalter ist es
			# nicht. nginx (lowercase) und Traefik sind davon nicht betroffen.
			@denied status 401
			handle_response @denied {
				redir {rp.header.X-Tinysesam-Location} 302
			}
			# angemeldet, aber Rolle fehlt → 403 stehen lassen. NICHT zum Login schicken:
			# der schickt denselben Benutzer sofort zurück, dem dieselbe Rolle fehlt.
			@norole status 403
			handle_response @norole {
				respond "Kein Zugriff: fehlende Rolle." 403
			}
		}
		# Die eigentliche Upstream-App — OHNE die Cookies von TinySesam (B-20).
		# Der Browser schickt es an jeden Host unter cookie_domain mit, und ungefiltert las es
		# jede geschützte App mit: Eine kompromittierte oder schlicht geschwätzige App (Debug-
		# Seite, Fehlerbericht, Log mit Anfrage-Headern) gab damit eine Sitzung heraus, die für
		# ALLE anderen Apps und für das Konto bei TinySesam gilt. Die App braucht das Cookie
		# nicht — wer angemeldet ist, sagen ihr die Remote-*-Header.
		# Dasselbe gilt für das Freigabe-Cookie `tinysesam_runlock` (entsperrte Ressourcen), und
		# das CSRF-Cookie ist der App ebenso wenig bestimmt — also alle `tinysesam_*`.
		# Die erste Zeile entfernt jedes Vorkommen (auch mit __Host-/__Secure-Präfix), die
		# zweite ein dabei übrig gebliebenes führendes `; `. Andere Cookies bleiben unberührt.
		# Umbenannte Cookies (TinySesamConfig(session_cookie=…, resource_cookie=…,
		# csrf_cookie=…)) hier ins Muster mitziehen.
		reverse_proxy {$TS_APP_UPSTREAM:127.0.0.1:9000} {
			header_up Cookie "(^|;)\s*(__Host-|__Secure-)?tinysesam_[^=;]*=[^;]*" ""
			header_up Cookie "^[;\s]+" ""
		}
	}
}
