Repository navigation
core: paint only the Project cards tasks would let a member open - #1725
Conversation
The `project-board` case in store.js passed only the Project id, so the board answered with every card under the Project while `tasks` refused the same member with `bot/forbidden`. A signed-in member could read another member's Project cards, and the ThinkFlowLab#1715 view made those rows clickable into the refusal. Pass the authenticated actor down, and drop rows in project-board.js through the one predicate tasks.js already owns (`rt.taskVisible`): the requester, the owner of the card's Bot or an ancestor, or an admin of the card's room. A host without the predicate paints nothing, an empty board is still `[]`, and the endpoint name, the authz allow-list and the row shape are unchanged. Test: core/test/project-board.test.js, which fails on the base with `expected [] / actual [task-b1]` when member A reads member B's Project.
|
Verdict: PASS at 9b48f09 Ran at this tip: Red first, and it is the leak. With the three changed sources reverted to their base One copy of the rule. Scope. CI at the tip. 14 of 15 runs green — Not run: Metadata. I corrected the |
Fixes #1723
Written-by: Dev-1
Owner: Dev-1
Reviewer: Birfy
Branch: dev/1-board-actor-filter
Base: f0159ea (live main)
Tip: 9b48f09
Tree: cf5297aeaa65c3470d8ac3522ab66dd714d630b8 (the same tree as the local commit 5222d149 that ran the checks; the push helper replayed the change onto live main, so only the commit identity differs)
What changed
store.js'sproject-boardcase calledrt.projectBoard(projectId)— the id only.project-board.jstherefore answered with every task whose board resolved to that Project, whoever was asking, whiletasksrefuses the same member withbot/forbidden("You cannot see that Bot's tasks"). Since#1716putproject-boardonauthz.js'sTASK_ENDPOINTS, any signed-in member could read another member's Project cards, and#1715's view made those rows clickable straight into that refusal — visible, unopenable.The actor now travels with the id, and the board drops rows through the predicate
tasks.jsalready owns, so the rule keeps one copy:core/src/host/store.js— theproject-boardcase passesactorintort.projectBoard(id, actor).core/src/host/tasks.js—taskVisible(task, actor)ismayAct(the card's requester) orancestorOwned(the owner of the card's Bot, or of an ancestor of it) orroomAdminof the room the card came from; it is installed asrt.taskVisiblebeside the helpers it composes.core/src/host/project-board.js— readsrt.taskVisibleandcontinues past a row it refuses. A host that did not install the predicate paints nothing (() => false): an unreadable rule is not a reason to widen a board.Not changed: the endpoint name,
authz.js's allow-list, the row shape, and an empty (or fully filtered) board still answers[]— a state, not an error. WithauthzEnforced()off,mayActkeeps answeringtrue, so the single-human install and the existing directrt.projectBoard('project-p1')reader behave exactly as before. The row-shape extension (board:{kind,id}, title/priority) stays in the P-167 backlog, as agreed.Red first
core/test/project-board.test.jsgains one case: a second owner (owner-2) with its own Botb, roomr2(adminb, Projectproject-p2), Projectproject-p2and a cardtask-b1on it. The four things the case holds, in the order they matter:project-board {id:'project-p2'}as member A is[], while today it is[task-b1].['task-b1'].#1716's same-owner ALLOW stays green: member A onproject-p1still gets['task-group'].ok:truewith[]— neverbot/forbidden— and the existingproject-emptycase is stillok:truewith[]. The test assertsokon both, so confusing "no permission" with "no cards" fails it.Member A also asks
tasks {botId:'b'}and getsbot/forbidden, which is the half that makes the leak a leak. The same file, on the basef0159ea8with this test added:So the leak is real on the base and it is a row, not a refusal: the member
tasksrefuses reads the card off the board. At this tip the same file is# tests 7 / # pass 7 / # fail 0— items 1–4 all hold,#1713's same-owner ALLOW included.Generated files
core/client.jsdoes not change — this is host code andnode core/scripts/build-client.mjs --checkis green, so gate 1 (build-client zero diff, listed on its own) is a no-op here:git status --short core/client.jsis empty after a full build.Gate 2 — this change's mirror:
patches/ds-bot/0002-flybot-api-host.patch(it carries thestore.jshunk) plusfiles/src/host/{project-board,tasks}.jsandfiles/test/project-board.test.js, all written bynode scripts/ds-bot-patches.mjs generatewith no hand-written hunk. Two consecutivegenerateruns leave those four paths byte-identical (same sha256), so the mirror is the generator's fixed point.Gate 3 — every other patch: on this branch
generatealso rewrites0001,0003,0005and0014in their hunk headers only (0 content lines:+45/−45,+24/−24,+13/−13,+2/−2) and0008by the bundle diff formatting (+4285/−2282) whilecore/client.jsis byte-identical to the base. That is the generator's drift, not this change, so those five are restored to the base bytes (git checkout HEAD --each) andnode scripts/ds-bot-patches.mjs checkis green; the branch's diff outside the change set is therefore zero. Conclusion line:ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)/ok 10 patches apply cleanly in order/ok core/ equals ds-bot/ + patches/ds-bot/.docs/subsystems/core-host.mdrecords the board's row rule next to the#1710paragraph it belongs to, soarch-docsneeds no trailer.changelog.d/1723-project-board-actor.security.mdis the user-visible fragment.What was run
Ran at this tip:
TZ=UTC ARCH_DOCS_BASE=f0159ea8c6c485df561572baee842fb8bb5aac76 npm run verify→ exit 0. Alsonode scripts/ds-bot-patches.mjs check(three green lines) andnode core/scripts/build-client.mjs --check(silent green), plus the single file under test:node --import ./test/harness-resolve.mjs --test test/project-board.test.js→ 7/7. Those ran at the treecf5297ae, which the pushed head9b48f0937fcarries byte for byte (git rev-parse: local5222d149treecf5297ae, remote9b48f0937ftreecf5297ae, single parentf0159ea8).Not run: the smoke test — this changes host authorization over an existing endpoint, not
core/index.jswiring or a new turn path;core/test/project-board.test.jsdrives the endpoint through the channel exactly as a member does.Checkpoint
{ "pr": 1725, "claim": "Dev-1", "issue": 1723, "summary": "The Project board now paints only the cards the asking member could open through `tasks`. `store.js` passes the authenticated actor into `projectBoard`, and the board drops a row `rt.taskVisible` refuses: the card's requester, the owner of its Bot or an ancestor, or an admin of its room. The predicate stays in tasks.js, so the rule has one copy; a host without it paints nothing, and an empty board is still []. The endpoint name, the authz allow-list and the row shape are unchanged.", "files": ["core/src/host/store.js", "core/src/host/tasks.js", "core/src/host/project-board.js", "core/test/project-board.test.js", "docs/subsystems/core-host.md", "changelog.d/1723-project-board-actor.security.md", "patches/ds-bot/0002-flybot-api-host.patch", "patches/ds-bot/files/src/host/project-board.js", "patches/ds-bot/files/src/host/tasks.js", "patches/ds-bot/files/test/project-board.test.js"], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=f0159ea8c6c485df561572baee842fb8bb5aac76 npm run verify", "result": "green", "at": "5222d149c9a1ca305e78481cd5f6fe276a7f8ddb" }, "next": "none — ready for review" }