# 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 {
	# Abmelden an der Anwendung (T-22): /.tinysesam/logout und /.tinysesam/after-logout gehören
	# TinySesam, nicht der Anwendung — und gehen am Gate vorbei: Abmelden muss auch mit
	# abgelaufener Anmeldung gehen. Auf diesem Host, weil nur hier das Gate-Cookie gelöscht werden
	# kann. Der Abmelde-Link der Anwendung zeigt auf https://app.example.com/.tinysesam/logout
	# (?scope=app|all, sonst gilt forward_logout); beim Provider als Logout Callback URL
	# https://app.example.com/.tinysesam/after-logout eintragen.
	handle /.tinysesam/* {
		reverse_proxy {$TS_AUTH_UPSTREAM:127.0.0.1:8000}
	}

	# Öffentliche Pfade der Anwendung (T-20), etwa Share-Links: ohne Anmeldung, ohne Identität.
	# TS_SHARE_PRAEFIX ist ein regulärer Ausdruck über den Anfang des Pfads, z. B.
	#   TS_SHARE_PRAEFIX='^/(s|public/share)/'
	# Ohne die Variable gilt `^$` — das trifft keinen Pfad, die Ausnahme ist aus.
	#
	# Geprüft wird der ROHE Pfad (`orig_uri.path`), nicht der von Caddy bereinigte: Caddy vergleicht
	# sonst `/x/../s/a` als `/s/a`, reicht der Anwendung aber `/x/../s/a` weiter — und die löst
	# ihn womöglich anders auf. Abgewiesen wird deshalb jeder Pfad mit `.`- oder `..`-Segment, `;`
	# (Tomcat/Spring lesen `/s/..;/admin` als `/admin`), Backslash, NUL oder einer kodierten Form
	# davon (%2e %2f %5c %00 %3b). Gross/klein zählt (`/S/` ist nicht `/s/`). Nachgemessen gegen
	# Caddy 2.11.7 in tests/test_gate_caddy.py. Nie nach Dateiendung freigeben (`*.js`): Viele
	# Anwendungen liefern unter `/admin/x.css` dynamischen Inhalt aus.
	#
	# Eine Share-Seite lädt meist eigene Skripte und ruft eigene APIs auf — auch die gehören in
	# den Ausdruck, je Anwendung nachgemessen. Was hier steht, ist öffentlich erreichbar: Eine Lücke
	# der Anwendung in diesen Pfaden bleibt offen. So eng wie möglich.
	@share {
		vars_regexp share {http.request.orig_uri.path} {$TS_SHARE_PRAEFIX:^$}
		not vars_regexp {http.request.orig_uri.path} (?i)(^|/)\.\.?(/|$)|[;\\\x00]|%(2e|2f|5c|00|3b)
	}
	handle @share {
		reverse_proxy {$TS_APP_UPSTREAM:127.0.0.1:9000} {
			# Keine Identität auf diesem Weg — auch keine, die der Browser selbst mitschickt.
			header_up -Remote-User
			header_up -Remote-Name
			header_up -Remote-Email
			header_up -Remote-Groups
			header_up -Remote-Id
			header_up Cookie "(^|;)\s*(__Host-|__Secure-)?tinysesam_[^=;]*=[^;]*" ""
			header_up Cookie "^[;\s]+" ""
		}
	}

	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}
				request_header Remote-Id {rp.header.Remote-Id}
			}
			# 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]+" ""
		}
	}
}
