Files
pad/internal/store
xarmian 83716a65fc fix(backlinks): scope cross-workspace admin enumeration to membership for bearer auth (BUG-1617) (#633)
Companion to BUG-1616. The admin platform role granted unrestricted
cross-workspace visibility at the STORE layer too: `GetCrossWorkspaceBacklinks`
ran `ListWorkspaces()` for any user with `Role=admin`, and
`ResolveBacklinksVisibility` short-circuited to `(nil, nil)` for the
same role check. Both fired BEFORE the BUG-1616 middleware gate could
deny the request, so a bearer-borne admin (CLI / PAT / MCP) could
enumerate cross-workspace backlinks from every workspace on the server.

Policy: bearer-borne admin gets STRICT membership enumeration — no
guest-grants fallback. Matches RequireWorkspaceAccess's membership-only
stance from BUG-1616. Cookie-session admin keeps the global view
(preserved web-UI affordance).

Threads `authIsBearer bool` from the HTTP boundary (via the new
isBearerAuth helper) into the store layer:

- `Store.ResolveBacklinksVisibility` — admin bypass now gated on
  `!authIsBearer`; bearer-admin falls through to the regular
  member/grants pipeline. Also tightens the "no visibility" return
  shape from `(nil, nil)` to non-nil empty slices so callers can
  distinguish "unrestricted" from "explicit empty" — closes a
  latent ambiguity that doesn't fire in current callers but would
  if any future caller bypassed the upstream membership filter.
- `Store.GetCrossWorkspaceBacklinks` — new switch:
    - cookie admin       → ListWorkspaces (unchanged)
    - bearer admin       → GetUserMemberWorkspaces (NEW, strict
                            membership; no grants fallback)
    - non-admin          → GetUserWorkspaces (unchanged; memberships
                            ∪ guest-grant workspaces)
- `Server.guestResourceFilterCore` — admin short-circuit now gated
  on `!isBearerAuth(r)`; bearer-admin delegates to the store-side
  helper with the bearer signal threaded through.
- `handlers_backlinks.go` — pass `isBearerAuth(r)` to
  `GetCrossWorkspaceBacklinks`.

New `Store.GetUserMemberWorkspaces` helper — the first half of
`GetUserWorkspaces` without the UNION-with-grants block. Used by the
bearer-admin path; existing callers continue to use `GetUserWorkspaces`
unchanged.

Tests:

- `wiki_links_xws_test.go`:
  - Updated `TestWikiLinks_CrossWorkspaceAdminSeesAllWorkspaces` to
    cover both `authIsBearer=false` (cookie, sees all) and
    `authIsBearer=true` (bearer, sees none) cases.
  - NEW `TestWikiLinks_CrossWorkspaceBearerAdminGrantOnlyWorkspaceFiltered`
    — bearer-admin with a guest grant on workspace C still gets ZERO
    cross-ws rows from C (Codex round-2 finding).
  - NEW `TestWikiLinks_CrossWorkspaceBearerAdminSeesMemberWorkspaces`
    — positive control: bearer-admin who IS a member sees the row.
  - Extended `TestResolveBacklinksVisibility_RoleMatrix` with two
    bearer-admin subtests (non-member workspace → empty; member
    workspace → unrestricted).
- NEW `handlers_backlinks_admin_bearer_test.go::TestCrossWorkspaceBacklinks_AdminBearer_OnlySeesMembershipWorkspaces`
  — full HTTP integration test, both cookie and PAT-bearer subtests.
- All existing callers updated to pass `false` for `authIsBearer`
  (preserves current cookie-session / non-admin behavior).

Verification: full `go test ./...` green; `make lint` clean;
Codex round 2 review CLEAN.

🤖 BUG-1617
2026-05-27 10:37:30 -04:00
..