diff --git a/zddc/internal/handler/profilehandler.go b/zddc/internal/handler/profilehandler.go index eed1e08..9421209 100644 --- a/zddc/internal/handler/profilehandler.go +++ b/zddc/internal/handler/profilehandler.go @@ -197,10 +197,22 @@ type AccessView struct { // silently ignored (the global fields still return). func enumerateAccess(ctx context.Context, decider policy.Decider, cfg config.Config, p zddc.Principal, pathQuery string) AccessView { view := AccessView{ - Email: p.Email, - EmailHeader: cfg.EmailHeader, - IsSuperAdmin: zddc.IsAdmin(cfg.Root, p), + Email: p.Email, + EmailHeader: cfg.EmailHeader, } + + // Path-scoped query: return ONLY the access for THIS location. The + // global summary (every project, every admin subtree) requires tree + // walks that are irrelevant to "what can I do here?" — and the + // hovercard calls this per folder, so paying that cost per hover + // would be wasteful. Callers that want the global view omit ?path=. + if pathQuery != "" { + populatePathScopedAccess(ctx, decider, cfg, p, pathQuery, &view) + return view + } + + // Global summary (the profile page). + view.IsSuperAdmin = zddc.IsAdmin(cfg.Root, p) view.Projects, _ = EnumerateProjects(ctx, decider, cfg, p) view.AdminSubtrees = enumerateAdminSubtrees(cfg, p) view.HasAnyAdminScope = view.IsSuperAdmin || len(view.AdminSubtrees) > 0 @@ -216,9 +228,6 @@ func enumerateAccess(ctx context.Context, decider policy.Decider, cfg config.Con allowed, _ := policy.AllowActionFromChainP(ctx, decider, rootChain, p, "/", policy.ActionCreate) view.CanCreateProject = allowed } - if pathQuery != "" { - populatePathScopedAccess(ctx, decider, cfg, p, pathQuery, &view) - } return view } diff --git a/zddc/internal/handler/profilehandler_test.go b/zddc/internal/handler/profilehandler_test.go index 28964e4..612249f 100644 --- a/zddc/internal/handler/profilehandler_test.go +++ b/zddc/internal/handler/profilehandler_test.go @@ -540,6 +540,12 @@ acl: if alice.PathVerbs != "rw" { t.Errorf("alice PathVerbs = %q, want rw", alice.PathVerbs) } + // A path-scoped query returns ONLY the access for this location — the + // global summary (projects + admin-subtree walks) is omitted. + if len(alice.Projects) != 0 || len(alice.AdminSubtrees) != 0 || alice.CanCreateProject { + t.Errorf("path-scoped response leaked global fields: Projects=%d AdminSubtrees=%d CanCreateProject=%v", + len(alice.Projects), len(alice.AdminSubtrees), alice.CanCreateProject) + } if alice.PathIsAdmin { t.Errorf("alice PathIsAdmin = true, want false") }