package handler import ( "net/http" "path/filepath" "strings" "codeberg.org/VARASYS/ZDDC/zddc/internal/config" "codeberg.org/VARASYS/ZDDC/zddc/internal/zddc" ) // ReservedSidecar is the single reserved namespace under any directory. ALL // server bookkeeping lives under /.zddc.d/ — bearer tokens // (.zddc.d/tokens), access logs (.zddc.d/logs), edit history // (.zddc.d/history), the apps cache (.zddc.d/apps) and the MD-conversion // cache (.zddc.d/converted). // // It is the one hard rule that overrides operator ACLs: a broad grant // (e.g. `*: rwcd`) must never expose the token store, so .zddc.d is admin-only // at every depth via this segment-name gate rather than a cascade ACL fence // (the path-tree cascade has no match-this-name-at-any-depth rule). Everything // else dot-prefixed is ordinary ACL-governed content; a leading dot only hides // an entry from directory listings (UI) — see internal/fs and internal/listing. // // The gate is applied on reads in dispatch (cmd/zddc-server) and mirrored on // the write path in authorizeAction. Bearer-token validation reads // .zddc.d/tokens directly from the filesystem in ACLMiddleware, before any of // this, so it is never affected by the HTTP-layer gate. const ReservedSidecar = ".zddc.d" // HasReservedSidecar reports whether any segment of urlPath is the reserved // .zddc.d sidecar. The comparison is case-insensitive: ZDDC_ROOT may sit on a // case-insensitive filesystem (SMB/CIFS/Azure Files), where `.ZDDC.D/tokens` // resolves to the same directory as `.zddc.d/tokens` — so the gate must catch // every case variant, not just the literal lowercase form, or a write to a // case-varied path (e.g. a MOVE destination that skips dispatch's canonical // case-folding) could reach the token store. func HasReservedSidecar(urlPath string) bool { for _, seg := range strings.Split(strings.Trim(urlPath, "/"), "/") { if strings.EqualFold(seg, ReservedSidecar) { return true } } return false } // ActiveAdminForSidecar reports whether the request's principal is an active // (elevated) admin with authority over the subtree that contains the first // .zddc.d segment of urlPath. It is only meaningful when // HasReservedSidecar(urlPath) is true. Root .zddc.d (e.g. /.zddc.d/tokens) // requires a root admin; a per-directory .zddc.d (e.g. // //.../.zddc.d/history) requires an admin over that subtree. Bearer // clients are implicitly elevated by ACLMiddleware, so a CLI caller with admin // authority passes. func ActiveAdminForSidecar(cfg config.Config, r *http.Request, urlPath string) bool { p := PrincipalFromContext(r) if !p.Elevated || p.Email == "" { return false } parent := make([]string, 0) for _, seg := range strings.Split(strings.Trim(urlPath, "/"), "/") { if strings.EqualFold(seg, ReservedSidecar) { break } parent = append(parent, seg) } dir := filepath.Join(cfg.Root, filepath.FromSlash(strings.Join(parent, "/"))) chain, _ := zddc.EffectivePolicy(cfg.Root, dir) return zddc.IsAdminForChain(chain, p.Email) }