diff --git a/zddc/internal/handler/fileapi.go b/zddc/internal/handler/fileapi.go index 41cf57b..0b9137d 100644 --- a/zddc/internal/handler/fileapi.go +++ b/zddc/internal/handler/fileapi.go @@ -118,16 +118,12 @@ func resolveTargetPath(cfg config.Config, urlPath string) (absPath, cleanURL str // that create a brand-new file inherit the parent directory's chain). // Returns allowed=false with the response status already written on deny. // -// Admin escape hatches: root admins (IsAdmin) and subtree admins -// (IsSubtreeAdmin) get unconditional access — the cascade evaluator -// and the WORM mask do not see their requests at all. This is the -// only way to mutate filed documents in Issued/Received. -// -// .zddc writes use the stricter CanEditZddc rule (strict-ancestor -// admin authority) regardless of the action verb, since the file -// being written is itself the source of the authority decision and -// the strict-ancestor rule is the existing defense against -// self-elevation. +// All admin / WORM / ACL logic lives downstream in the decider's single +// bypass site (policy.InternalDecider.Allow). AllowActionFromChainP +// computes IsActiveAdmin from the chain and Principal.Elevated, with +// the strict-ancestor rule applied when action == ActionAdmin (the +// caller tags .zddc writes that way). The handler does NOT make +// admin/elevation decisions of its own — one bypass site, one helper. func authorizeAction(cfg config.Config, w http.ResponseWriter, r *http.Request, absPath, urlPath, action string) bool { probe := filepath.Dir(absPath) for { @@ -142,40 +138,14 @@ func authorizeAction(cfg config.Config, w http.ResponseWriter, r *http.Request, probe = filepath.Dir(probe) } - email := EmailFromContext(r) p := PrincipalFromContext(r) - - // Admin bypass — root and subtree. Un-elevated admins fall through - // to the regular decider; the Principal's Elevated flag short- - // circuits both IsAdmin and IsSubtreeAdmin so we don't accidentally - // hand out WORM bypass to a user who hasn't asked for it. - if zddc.IsAdmin(cfg.Root, p) { - return true - } - if zddc.IsSubtreeAdmin(cfg.Root, probe, p) { - return true - } - - // .zddc writes: CanEditZddc enforces the strict-ancestor rule that - // prevents a subtree admin from elevating themselves by editing the - // .zddc that grants their authority. Non-admins (or un-elevated - // admins) fall through to the regular decider — they will be denied - // unless an explicit `a` verb is granted to a non-admin role at - // this path, which is unusual. - if filepath.Base(absPath) == ".zddc" { - zddcDir := filepath.Dir(absPath) - if zddc.CanEditZddc(cfg.Root, zddcDir, p) { - return true - } - } - chain, err := zddc.EffectivePolicy(cfg.Root, probe) if err != nil { slog.Warn("file API ACL chain error", "path", absPath, "err", err) } decider := DeciderFromContext(r) - allowed, _ := policy.AllowActionFromChain(r.Context(), decider, chain, email, urlPath, action) + allowed, _ := policy.AllowActionFromChainP(r.Context(), decider, chain, p, urlPath, action) if !allowed { http.Error(w, "Forbidden", http.StatusForbidden) return false diff --git a/zddc/internal/handler/planreview.go b/zddc/internal/handler/planreview.go index ba2c4fa..628b5a1 100644 --- a/zddc/internal/handler/planreview.go +++ b/zddc/internal/handler/planreview.go @@ -157,21 +157,34 @@ func executePlanReview(cfg config.Config, r *http.Request, project, party, track if email == "" { return nil, http.StatusForbidden, "Forbidden — no authenticated principal" } + // All three pre-flight checks go through the consolidated decider. + // AllowActionFromChainP applies the strict-ancestor rule for + // .zddc-targeted actions (ActionAdmin) and the single admin-bypass + // branch for elevated admins. No manual IsAdmin / IsSubtreeAdmin / + // CanEditZddc branching here. + decider := DeciderFromContext(r) for _, root := range []string{reviewingRoot, stagingRoot} { - if !zddc.CanEditZddc(cfg.Root, root, p) { + chain, perr := zddc.EffectivePolicy(cfg.Root, root) + if perr != nil { + return nil, http.StatusInternalServerError, "Internal Server Error — cascade lookup: " + perr.Error() + } + rel, _ := filepath.Rel(cfg.Root, root) + rootURL := "/" + filepath.ToSlash(rel) + "/.zddc" + allowed, _ := policy.AllowActionFromChainP(r.Context(), decider, chain, p, rootURL, policy.ActionAdmin) + if !allowed { return nil, http.StatusForbidden, fmt.Sprintf("Forbidden — %s lacks subtree-admin authority for %s", email, strings.TrimPrefix(root, cfg.Root+string(filepath.Separator))) } } - // (b) — verify `c` authority on received//. Admins bypass - // the policy and would pass anyway; non-admin doc_controllers come - // through the WORM-list grant. - if !zddc.IsAdmin(cfg.Root, p) && !zddc.IsSubtreeAdmin(cfg.Root, receivedAbs, p) { + // Verify `c` (create) authority on received//. Elevated + // admins short-circuit inside the decider; non-admin doc_controllers + // come through the WORM-list grant. One code path either way. + { chain, perr := zddc.EffectivePolicy(cfg.Root, receivedAbs) if perr != nil { return nil, http.StatusInternalServerError, "Internal Server Error — cascade lookup: " + perr.Error() } - allowed, _ := policy.AllowActionFromChain(r.Context(), DeciderFromContext(r), chain, email, cleanURL, policy.ActionCreate) + allowed, _ := policy.AllowActionFromChainP(r.Context(), decider, chain, p, cleanURL, policy.ActionCreate) if !allowed { return nil, http.StatusForbidden, fmt.Sprintf("Forbidden — %s lacks create authority on %s (filing this submittal requires the doc_controller WORM grant)", email, strings.TrimPrefix(receivedAbs, cfg.Root+string(filepath.Separator)))