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:
parent
2ccd72fa35
commit
8ffbcb90d1
2 changed files with 85 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue