feat(handler): expose inherit fence in /.profile/effective-policy

The cascade tracer's JSON response now carries:

- Top-level `cascade_mode` (string): the active mode (delegated /
  strict). Helps reviewers correlate the visible_start with the mode.
- Top-level `chain.visible_start` (int): chain.VisibleStart(leaf, mode)
  — the lowest level whose grants the leaf can see, accounting for any
  inherit:false fence in delegated mode (always 0 in strict mode).
- Per-level `inherit` (*bool, omitempty): the level's explicit inherit
  value, nil when absent. A reviewer can scan the levels and see which
  one fences ancestors.

The level's `exists` flag now also fires for `permissions:` and
`inherit:` entries (previously it only checked Allow/Deny/Admins),
so the response correctly reflects modern .zddc files that use the
permissions map.

Test: TestServeProfileEffectivePolicy_InheritFence builds a vendor-
folder layout, asks the tracer about a my-company user, confirms
decision=false, visible_start=1 (fence at /Vendor/), leaf.Inherit=
&false, root.Inherit=nil.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-07 11:02:33 -05:00
parent 2ccd72fa35
commit 8ffbcb90d1
2 changed files with 85 additions and 2 deletions

View file

@ -371,6 +371,10 @@ func serveProfileEffectivePolicy(cfg config.Config, w http.ResponseWriter, r *ht
Admins []string `json:"admins,omitempty"`
AnyMatch bool `json:"matches_email"`
Decision string `json:"decision_at_level"`
// Inherit is the level's explicit inherit setting if present
// (nil for absent — defaults to "inherit normally"). When
// false, this level fences ancestors above it from descendants.
Inherit *bool `json:"inherit,omitempty"`
}
// Build the per-level breakdown by walking the chain levels in
@ -384,8 +388,14 @@ func serveProfileEffectivePolicy(cfg config.Config, w http.ResponseWriter, r *ht
Email string `json:"email"`
Decision bool `json:"decision"`
DeciderKind string `json:"decider_kind"`
CascadeMode string `json:"cascade_mode"`
Chain struct {
HasAnyFile bool `json:"has_any_file"`
// VisibleStart is the lowest chain index whose grants are
// visible to evaluation at the leaf, accounting for any
// inherit:false fence in delegated mode. In strict mode it
// is always 0 (fences are ignored under AC-6).
VisibleStart int `json:"visible_start"`
Levels []levelView `json:"levels"`
} `json:"chain"`
}{
@ -393,8 +403,11 @@ func serveProfileEffectivePolicy(cfg config.Config, w http.ResponseWriter, r *ht
Email: probeEmail,
Decision: allow,
DeciderKind: deciderKind(decider),
CascadeMode: cfg.CascadeMode,
}
out.Chain.HasAnyFile = chain.HasAnyFile
mode, _ := zddc.ParseCascadeMode(cfg.CascadeMode)
out.Chain.VisibleStart = chain.VisibleStart(len(chain.Levels)-1, mode)
// Reconstruct level paths from cfg.Root. This mirrors how
// zddc.EffectivePolicy builds the chain (see cascade.go).
@ -425,7 +438,8 @@ func serveProfileEffectivePolicy(cfg config.Config, w http.ResponseWriter, r *ht
entry := levelView{
Index: i,
ZddcPath: lp,
Exists: len(lvl.Admins) > 0 || len(lvl.ACL.Allow) > 0 || len(lvl.ACL.Deny) > 0,
Exists: len(lvl.Admins) > 0 || len(lvl.ACL.Allow) > 0 || len(lvl.ACL.Deny) > 0 || len(lvl.ACL.Permissions) > 0 || lvl.ACL.Inherit != nil,
Inherit: lvl.ACL.Inherit,
}
if entry.Exists {
entry.Acl = &lvl.ACL

View file

@ -564,6 +564,75 @@ func TestServeProfileEffectivePolicy(t *testing.T) {
}
}
// TestServeProfileEffectivePolicy_InheritFence: a child .zddc with
// inherit:false fences ancestor grants. The tracer surfaces both the
// per-level inherit flag and the chain-level visible_start so an
// operator can see why ancestor grants don't apply at the leaf.
func TestServeProfileEffectivePolicy_InheritFence(t *testing.T) {
cfg, ring := profileTestRoot(t, []string{"super@admin.com"})
// Vendor-folder pattern: root grants everyone-at-mycompany rwcd;
// the vendor folder fences and only allows the vendor. Preserve
// the admins: list so the test admin can hit the tracer.
if err := os.WriteFile(filepath.Join(cfg.Root, ".zddc"),
[]byte("admins:\n - super@admin.com\nacl:\n permissions:\n \"*@mycompany.com\": rwcd\n"),
0o644); err != nil {
t.Fatalf("write root .zddc: %v", err)
}
if err := os.MkdirAll(filepath.Join(cfg.Root, "Vendor"), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(filepath.Join(cfg.Root, "Vendor", ".zddc"),
[]byte("acl:\n inherit: false\n permissions:\n \"*@vendor.com\": rwcd\n"),
0o644); err != nil {
t.Fatalf("write vendor .zddc: %v", err)
}
zddc.InvalidateCache(cfg.Root)
type respShape struct {
Decision bool `json:"decision"`
CascadeMode string `json:"cascade_mode"`
Chain struct {
VisibleStart int `json:"visible_start"`
Levels []struct {
Index int `json:"index"`
Inherit *bool `json:"inherit,omitempty"`
} `json:"levels"`
} `json:"chain"`
}
// Trace a my-company user — fenced out at the leaf, despite root grant.
rec := httptest.NewRecorder()
ServeProfile(cfg, ring, nil, rec,
requestWithEmail(http.MethodGet,
"/.profile/effective-policy?path=/Vendor/&email=alice@mycompany.com",
"super@admin.com"))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
var got respShape
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got.Decision {
t.Errorf("alice should be fenced out; decision = true")
}
if got.Chain.VisibleStart != 1 {
t.Errorf("VisibleStart = %d, want 1 (fence at /Vendor/)", got.Chain.VisibleStart)
}
if len(got.Chain.Levels) != 2 {
t.Fatalf("expected 2 levels; got %d", len(got.Chain.Levels))
}
leaf := got.Chain.Levels[1]
if leaf.Inherit == nil || *leaf.Inherit != false {
t.Errorf("leaf.Inherit should be explicit false; got %v", leaf.Inherit)
}
root := got.Chain.Levels[0]
if root.Inherit != nil {
t.Errorf("root.Inherit should be unset; got %v", root.Inherit)
}
}
func TestServeProfileNoAdminsConfiguredStillRendersPage(t *testing.T) {
// .zddc exists but has no admins list — page is still reachable,
// but the admin/super-admin sections are absent.