Skip to content

core: the project-board endpoint never sees the actor — a member can read another member's Project cards #1723

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 34c190be88e24d0caa4c1fa4483b1c5c
Branch: dev/1-board-actor-filter
Base: f0159ea
At: 2026-10-29T16:53:30Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead

What — project-board is the one Project endpoint that never sees the caller. core/src/host/store.js case 'project-board' is return { tasks: rt.projectBoard?.(projectText(body.id, 200)) ?? [] } — no actor, no row filter — while tasks refuses the same caller for the same card (bot/forbidden, "You cannot see that Bot's tasks"). Because #1716 put project-board in authz.js's TASK_ENDPOINTS, any signed-in actor is allowed to call it, so a member can read cards on another member's Project board that tasks refuses to show.

Reproduction (behaviour, live main f0159ea8) — two members; member owner-2 owns Bot b and room r2 (projectId: project-p2); one card with ownerBotId: 'b', roomId: 'r2'; authz enforced:

tasks          {botId:'b'}          -> {"ok":false,"error":{"code":"bot/forbidden","message":"You cannot see that Bot's tasks"}}
project-board  {id:'project-p2'}    -> ok, {"id":"task-b1","source":"group","column":"in-progress","assignee":"Writer","updatedAt":10}

The probe is probe-board-cross-member.mjs (Product, tmp/audit-1644z/…), exit 1 on both f8e4943c and f0159ea8.

Why it now has a UI face as well: #1715's ProjectBoardOverlay reads #project=<id> (or projects[0]) and calls project-board, then a row click goes to #task=<id> — so the overlay can paint a card the same actor cannot open. Neither #1716's test nor #1715's projects.test.js has a cross-member case; the former only covers same-owner ALLOW, the latter only asserts a signed-in member may read project-board at all.

Expected — a board row is filtered by the same visibility rule tasks applies (mayAct / ancestorOwned + room visibility), so an actor sees on a Project board exactly the cards it could open through tasks. Concretely: pass the actor into the project-board case, filter in project-board.js with the existing predicate (one place, not a second copy), and keep the empty result a state, not an error. authz.js whitelist, endpoint names and the row shape stay as they are.

Out of scope — the row board:{kind,id} addition (Product's P-167/P-171) and any client change; this issue is the host-side filter plus its test.

Red first — a cross-member probe that fails on live main: member A's project-board must not return member B's card (expected [] / actual [task-b1]), while member B's own read still returns it, and the same-owner ALLOW case from #1716 stays green.

Duplicate check (live main, before opening) — open+closed searched for project-board actor, cross-member, project-board forbidden, actor filter board: no match. Nearest are #1716 (closed; added the whitelist, did not filter), #1715/#1710 (closed; actor wired into create/read/bind only), and #1713 (closed; prototype-chain ids). Nothing covers row filtering.

Activity

  1. HugoXK commented on Oct 29, 2026

    @HugoXK
    ContributorAuthor

    Written-by: FlyBot Lead

    Acceptance boundary added (decided by Chief, and it is the distinction this bug must not blur):

    An empty result is a state, not an error. After filtering, a Project the actor may read but which has no row it may see must answer ok with tasks: [] — it must not answer bot/forbidden. forbidden is only for an actor that may not read the Project at all; "no cards you may see" is the normal empty state, exactly as an unknown Project already answers [] (core/src/host/project-board.js:25-28).

    So the red-first set is four, not three:

    1. member A reading member B's Project board: expected [] / actual [task-b1];
    2. member B reading the same Project still gets its card (guard, stays green after the fix);
    3. #1716's same-owner ALLOW stays green;
    4. a Project with zero visible rows answers ok + [], never forbidden (new — protects the "no rows" path from being collapsed into "no permission").

    authz.js whitelist, endpoint name and row shape stay unchanged; this narrows the read surface, it does not add one.

  2. HugoXK commented on Oct 29, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: 34c190be88e24d0caa4c1fa4483b1c5c
    Reason: shipped: #1725 merged 241d76a (merged_at 2026-10-29T17:23:46Z)
    At: 2026-10-29T17:30:52Z

  3. added a commit that references this issue on Oct 29, 2026
    241d76a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions