Skip to content

kanban: honour the projectId CreateRule and GetRules already require - #381

Merged
Yaraslaut merged 1 commit into
masterfrom
fix/369-kanban-project-id
Sep 1, 2026
Merged

kanban: honour the projectId CreateRule and GetRules already require#381
Yaraslaut merged 1 commit into
masterfrom
fix/369-kanban-project-id

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Closes #369.

GetRules carried a projectId, validate() insisted it be present, and the implementation never consulted it — the query was built from the handler's attached board. A handler attached to project A, asked for project B's rules, answered with A's, successfully and silently. The argument was load-bearing in the type and inert in the code.

CreateRule had it too, and it is a write

The issue asked for CreateRule to be checked for the same thing. It has it, and it is worse: attached to project A with projectId=B and A's own trigger column, CreateRule created the rule on A and returned its id. Measured, not inferred — pre-fix, CHECK(GetRules{A}.rules.empty()) came back false after a create that should have been refused.

board_model.cpp carries exactly three actions with a projectId. The third, OpenBoard, is correct as it stands: it is the keyed attach action (BRIDGE_MODEL_KEY), and its id is what the other two are now compared against. ProjectAdminModel's three (SetMemberRole, RemoveMember, GetProjectRoles) are a different file and already mean it.

The fix

A new requireProjectMatchesAttachedBoard, sibling of the existing require*BelongsToProject helpers in the same file, throwing NotFound{"projectId does not match the attached board"}. Both call sites sit after the attach and requireRole gates, so the new refusal cannot be used to probe projects the caller has no standing on. CreateRule's sits inside its existing try, so the refusal is journalled Failed like every other refusal there.

The doc comments that asserted the old behaviour are corrected rather than left to contradict the code: board_model.hpp's @param action Unused — not consulted, both DTOs in rule_dto.hpp, and board_presenter.hpp's _projectId comment. The GUI is unaffected — BoardPresenter::openBoard() stashes exactly the id it opens with.

Verification

Reproduced in-process, pre-fix: test cases: 7 | 5 passed | 2 failed, both because no exception was thrown where one was expected, plus the CreateRule write landing on the wrong project.

Reproduced on the wire, pre-fix server, GetRules projectId=20 while attached to project 19:

FAIL step 16 (line 75)
  expected: @kind == err
  actual:   @kind == ok
  reply: ok body={"rules":[{"id":6,...},{"id":7,...}]}

Post-fix: [cross-tenant] All tests passed (11 assertions in 7 test cases); full rung All tests passed (1025 assertions in 139 test cases). run_scenarios.py --rung kanban --twice green on both passes; --mutate reports no surviving mutants, so the new assertions are load-bearing. test_morph_scenario.py and scenario_coverage.py both exit 0, kanban still 22/22 actions and 20/20 workflows.

scripts/scenario/scenarios/kanban/rules-created-listed-and-deleted.scenario pinned the old behaviour on purpose and cited this issue. It now asserts the refusal — for GetRules and for the CreateRule write — and its header comment describes the fixed behaviour.

Not verified: the kanban test binary could not be built under -Werror in the agent's worktree (#372/#376 — AUTOMOC's relative include escaping a nested worktree, unrelated to this change), so the tests were run from a non-strict build dir. board_model.cpp itself was rebuilt under the full strict config and links clean. No GUI run beyond the presenter suite.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GnSnsebJ92Kti1D7m3RwYJ

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
examples/kanban/src/models/board_model.cpp 80.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Both DTOs carry a projectId their own validate() insists be present, and
BoardModel::execute() consulted neither: the board came from the handler's
attach state alone. A handler attached to project A, asked for project B's
rules, answered with A's and an ok; asked to create a rule on B, it created
one on A and reported its id. Neither answer could be told apart from having
been served, so a client that passed the id it meant was reading -- or
writing -- the wrong board with no way to notice.

The argument is now checked against the attached board and a mismatch refused
with "projectId does not match the attached board", in the same shape, error
type and message style as this file's other cross-project refusals ("rule
does not belong to this project", "column does not belong to this project").
The field is honoured rather than dropped because GetRules' sibling reads on
ProjectAdminModel (GetProjectRoles) take a projectId and mean it.

OpenBoard is the third action in this model taking a projectId and always
meant it -- it is the keyed attach action, and it is what the check compares
against. No other action here carries one.

Nothing was disclosed that a caller could not already read: the handler is
keyed by project, and requireRole runs against the attached board before the
new check, so the refusal cannot be used to probe projects the caller has no
standing on.

scripts/scenario/scenarios/kanban/rules-created-listed-and-deleted.scenario
pinned the old behaviour on purpose and cited this defect; it now pins the
refusal, on the writing half as well as the reading one.

Fixes #369

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnSnsebJ92Kti1D7m3RwYJ
@Yaraslaut
Yaraslaut force-pushed the fix/369-kanban-project-id branch from 2fc000a to 47f2160 Compare September 1, 2026 15:43
@Yaraslaut
Yaraslaut merged commit 10039f2 into master Sep 1, 2026
25 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kanban: GetRules requires a projectId and then ignores it

1 participant