Guard card edits #98

Closed
opened 2026-08-18 02:05:50 +00:00 by Grandiras · 2 comments
Owner

Only let authors and admins edit cards - not everyone. Also add comment editing and moderation. So basically real permissions and scopes.

Only let authors and admins edit cards - not everyone. Also add comment editing and moderation. So basically real permissions and scopes.
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/266); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-98/run-266.md) — updating every ~5 minutes while the task runs.
Collaborator

Pushed as claude/issue-98.

What changed

Services/CardPermissions.cs is the one place the rules live. It takes the same BoardViewer BoardAccess does, because "a DID, and whether it administers this deployment" is the whole of what either question needs — BoardAccess decides which boards you may see, this decides what you may do to the things on one. A second actor type built from the same allowlist snapshot would just be a second thing to keep in step.

who
edit / delete a card (incl. tags, assignees, due date) its author, or an admin
move a card between columns anybody who can see the board
create a card, comment on any card anybody who can see the board
edit a comment its author — admins included in the "no"
delete a comment its author, or an admin

Two of those are deliberate refusals to over-guard. Moving a card changes which column it is in and not a word of what it says — locking that to the author would make a kanban board a wall of other people's cards nobody may sort. And an admin who could edit a comment could change what a member is recorded as having said, under that member's name and avatar; moderation takes something down, it does not rewrite it. Tags and assignees, by contrast, follow the card's own rule, because they are saved by the same button — a guard you can walk round by re-tagging is not a guard.

It is enforced in the services (PermissionDeniedException, which the pages turn into translated copy), not only in the markup — that is the difference between a permission and a hidden button, and it is what catches a circuit that had the dialog open when the rights changed. BoardPage, Calendar and CommentList rebuild their viewer where they reload their data, so a demotion lands on a page that is already open rather than at the next reconnect.

Comment editing adds Comment.EditedAt (migration AddCommentEditedAt, null for every comment that predates it, no backfill needed). It is stamped only on a real change, and shown beside the time — a reply underneath may be answering words that are no longer there.

Somebody else's card, as an ordinary member — the fields are text, the save and delete buttons are gone, one note says why, and the discussion below is still theirs:

The card dialog opened by a member who did not create the card: a note explaining the card is read-only, the title and description as text, tags and due date as labels, and a full comment box with Edit and Delete on their own comment only

The same card for its author (an admin here), hovering the other member's comment — one action on it, and it is the removing one:

The same card dialog for its author: editable title and description, tag and assignee pickers, a date input, Save and Delete in the footer, and the other member's comment showing only a Delete action

Verified

  • dotnet build clean (one pre-existing nullability warning in Calendar.razor:287, untouched by this).
  • dotnet test Ponente.sln282 passed, including the new CardPermissionsTests and the existing en/de key-parity and changelog guards.
  • The new tests/e2e/permissions.spec.js flow run and passing against a real running instance (Release build, Postgres, migration applied on startup): read-only dialog for a non-author, comment posted and rewritten by its author with the "edited" mark appearing, the member's own card fully editable, an admin editing that card, and an admin moderating the member's comment away. The container has no runtime for the PDS or the compose stack, so the second member came from a row in Members plus /dev/login instead of a PDS signup; the committed spec uses access.spec.js's signUpMember helper as the suite requires, and everything else in it is what was exercised. It has not been run under global-setup/global-teardown.

Two traps the spec documents, both hit while writing it: the comment box is a textarea inside .modal-body too, so "no editable fields" has to be scoped to .composer; and a comment being edited stops matching a hasText filter, since a textarea's value is not text content — hence data-comment-id on the article.

Also added: a changelog entry for 2.6.0 in both locales, a Who may change what section in CLAUDE.md, and a convention line that a new card/comment mutation takes a BoardViewer with no default argument.


⏱ 26m 55s · 104 turns · tokens: 213.5k in / 75.0k out (+17109.2k cache-read)
📜 Full session transcript
→ Opened PR: #100

Pushed as **`claude/issue-98`**. ## What changed `Services/CardPermissions.cs` is the one place the rules live. It takes the same `BoardViewer` `BoardAccess` does, because "a DID, and whether it administers this deployment" is the whole of what either question needs — `BoardAccess` decides which boards you may *see*, this decides what you may *do* to the things on one. A second actor type built from the same allowlist snapshot would just be a second thing to keep in step. | | who | |---|---| | edit / delete a card (incl. tags, assignees, due date) | its author, or an admin | | **move** a card between columns | anybody who can see the board | | create a card, comment on any card | anybody who can see the board | | **edit** a comment | its author — **admins included in the "no"** | | delete a comment | its author, or an admin | Two of those are deliberate refusals to over-guard. Moving a card changes which column it is in and not a word of what it says — locking that to the author would make a kanban board a wall of other people's cards nobody may sort. And an admin who could *edit* a comment could change what a member is recorded as having said, under that member's name and avatar; moderation takes something down, it does not rewrite it. Tags and assignees, by contrast, follow the card's own rule, because they are saved by the same button — a guard you can walk round by re-tagging is not a guard. It is enforced in the services (`PermissionDeniedException`, which the pages turn into translated copy), not only in the markup — that is the difference between a permission and a hidden button, and it is what catches a circuit that had the dialog open when the rights changed. `BoardPage`, `Calendar` and `CommentList` rebuild their viewer where they reload their data, so a demotion lands on a page that is already open rather than at the next reconnect. Comment editing adds `Comment.EditedAt` (migration `AddCommentEditedAt`, null for every comment that predates it, no backfill needed). It is stamped only on a real change, and shown beside the time — a reply underneath may be answering words that are no longer there. Somebody else's card, as an ordinary member — the fields are text, the save and delete buttons are gone, one note says why, and the discussion below is still theirs: ![The card dialog opened by a member who did not create the card: a note explaining the card is read-only, the title and description as text, tags and due date as labels, and a full comment box with Edit and Delete on their own comment only](https://git.grandiras.net/attachments/b0931f99-3e92-422b-93f3-fc9b07d426f2) The same card for its author (an admin here), hovering the other member's comment — one action on it, and it is the removing one: ![The same card dialog for its author: editable title and description, tag and assignee pickers, a date input, Save and Delete in the footer, and the other member's comment showing only a Delete action](https://git.grandiras.net/attachments/5009ce67-9274-454b-bd6c-fd8516b9953c) ## Verified - `dotnet build` clean (one pre-existing nullability warning in `Calendar.razor:287`, untouched by this). - `dotnet test Ponente.sln` — **282 passed**, including the new `CardPermissionsTests` and the existing en/de key-parity and changelog guards. - The new `tests/e2e/permissions.spec.js` flow **run and passing** against a real running instance (Release build, Postgres, migration applied on startup): read-only dialog for a non-author, comment posted and rewritten by its author with the "edited" mark appearing, the member's own card fully editable, an admin editing that card, and an admin moderating the member's comment away. The container has no runtime for the PDS or the compose stack, so the second member came from a row in `Members` plus `/dev/login` instead of a PDS signup; the committed spec uses `access.spec.js`'s `signUpMember` helper as the suite requires, and everything else in it is what was exercised. It has not been run under `global-setup`/`global-teardown`. Two traps the spec documents, both hit while writing it: the comment box is a `textarea` inside `.modal-body` too, so "no editable fields" has to be scoped to `.composer`; and a comment being edited stops matching a `hasText` filter, since a textarea's value is not text content — hence `data-comment-id` on the article. Also added: a changelog entry for **2.6.0** in both locales, a *Who may change what* section in `CLAUDE.md`, and a convention line that a new card/comment mutation takes a `BoardViewer` with no default argument. --- ⏱ 26m 55s · 104 turns · tokens: 213.5k in / 75.0k out (+17109.2k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-98/run-266.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/100
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente#98
No description provided.