ZDDC/zddc/internal/handler/sidecar.go
2026-06-11 13:32:31 -05:00

70 lines
3 KiB
Go

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 <dir>/.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.
// /<proj>/.../.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)
}