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

37 lines
1.7 KiB
Go

package handler
import (
"path/filepath"
"strings"
"codeberg.org/VARASYS/ZDDC/zddc/internal/apps"
"codeberg.org/VARASYS/ZDDC/zddc/internal/policy"
)
// configWriteAction returns the action a write to absPath must be authorized
// as. The .zddc cascade file and the .zddc.zip bundle are policy, not content:
// mutating either is a VerbA operation requiring standing config-edit authority
// (IsConfigEditor — a subtree admin or `a`-verb holder, no elevation), which
// the decider enforces when the action is tagged ActionAdmin. For any other
// path the supplied default action is returned unchanged.
//
// This is the single predicate behind the per-verb escalation that previously
// lived inlined in serveFilePut/serveFileDelete (.zddc only) and was MISSING
// from serveFileMove — letting a MOVE plant or relocate a policy file with mere
// create/write authority. PUT/DELETE on a URL-visible .zddc.zip are also
// existence-gated to config-editors at dispatch (the bundle visibility gate in
// cmd/zddc-server), but a MOVE destination rides in the X-ZDDC-Destination
// header and never reaches that gate — so the authority bar must be enforced
// here, on the resolved target path, for every write verb.
//
// Matching is case-insensitive to align with HasReservedSidecar: ZDDC_ROOT may
// sit on a case-insensitive filesystem (SMB/CIFS/Azure Files) where `.ZDDC` /
// `.ZDDC.ZIP` resolve to the same files, and a case-varied target must not slip
// past the gate.
func configWriteAction(absPath, def string) string {
base := filepath.Base(absPath)
if strings.EqualFold(base, ".zddc") || strings.EqualFold(base, apps.BundleName) {
return policy.ActionAdmin
}
return def
}