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 }