ZDDC/zddc/internal/handler/authcheck.go
ZDDC 6cc0d2ae27 feat(zddc-server): /.auth/admin forward_auth endpoint
A machine-only HTTP endpoint that returns 200 if the request's
X-Auth-Request-Email is in the root .zddc admins: list, 403 otherwise.
No body, no redirect — pure authorization decision intended to be
polled by an upstream proxy's forward_auth directive.

The motivating use case is gating /devshell/* (code-server) in the
dev-shell pod on root-admin status before the request ever reaches
code-server, which has no built-in ACL of its own. zddc-server's
own routes keep the existing .zddc cascade ACL and don't go through
this endpoint.

Reuses zddc.IsAdmin (one cached map lookup) so the check is cheap
enough to call on every request. Edits to /srv/.zddc propagate via
the existing fsnotify watcher's policy-cache invalidation.

Tests cover empty email, non-admin, admin, and the bootstrap state
where no root .zddc exists (deny everyone — the safe default).

Docs: zddc/README.md "Forward-auth target for upstream proxies"
section + AGENTS.md notes bullet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:08:39 -05:00

47 lines
1.9 KiB
Go

package handler
import (
"net/http"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// AuthPathPrefix is the URL prefix at which machine-only auth-check
// endpoints live. Mirrors ProfilePathPrefix's dot-prefix convention so
// the dispatch's reserved-prefix guard sees it as an internal namespace
// rather than user content.
const AuthPathPrefix = "/.auth"
// ServeAuthAdmin is a forward_auth target for upstream proxies (e.g. the
// dev-shell pod's Caddy in front of code-server). It returns:
//
// - 200 OK — caller's resolved email is in the root .zddc's
// admins: list, per zddc.IsAdmin.
// - 403 Forbidden — anonymous, or email not in the admins: list, or
// no root .zddc exists. Also covers the case where
// the admins: field is empty/missing.
//
// The endpoint produces no body and does not redirect — it's a pure
// authorization decision intended to be polled by Caddy's forward_auth
// directive (or any equivalent in nginx, Traefik, oauth2-proxy, etc.).
//
// Performance: zddc.IsAdmin is a single map lookup against a cached
// PolicyChain; the .zddc file is parsed once and re-read only when the
// fsnotify watcher fires. Suitable to call on every request without
// noticeable overhead.
//
// Scope: gates ON ROOT-ADMIN STATUS ONLY. This is intentionally
// stricter than the regular acl.allow / acl.deny chain — admin-only
// endpoints (the dev-shell IDE, future maintenance routes) shouldn't
// fall through to subtree-level allowances. For per-route ACL, callers
// continue using the existing handlers (archive, profile, etc.) which
// consult AllowedWithChain.
func ServeAuthAdmin(cfg config.Config, w http.ResponseWriter, r *http.Request) {
email := EmailFromContext(r)
if email == "" || !zddc.IsAdmin(cfg.Root, email) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
w.WriteHeader(http.StatusOK)
}