feat: redefine viewer as true read-only, promote collaborator to participation tier (#246 follow-on)

Background

Full RBAC audit (#246 (closed)) revealed that collaborator and viewer are treated identically in every permission check. The code pattern throughout boards/views.py is:

if role in (BoardMembership.Role.VIEWER, BoardMembership.Role.COLLABORATOR):
    raise PermissionDenied(...)

This means a collaborator cannot do anything a viewer cannot. The role is a trap — anyone assigned collaborator expecting elevated access over viewer gets none.

Additionally, viewer currently allows posting comments, uploading attachments, deleting attachments, and managing checklist items. These are participatory write actions that a read-only viewer should not have.

Note: docs/features/rbac/roles.md and docs/api/cards.md already describe the intended (tighter) behavior — collaborator can comment, viewer cannot. This is a fix to close the gap between docs and code, not a new design decision. Frame the changelog entry as fix:, not feat:.

Current state (before)

Action viewer collaborator member admin
View board, cards, columns, swimlanes
View movements & activities
View archived cards
Post comments
Delete own comments
Upload attachments
Delete attachments
Add / edit / delete checklist items
Create / edit / move / delete cards
Archive / unarchive cards
Columns / swimlanes / labels
Member management

collaborator and viewer are identical in every check.

Proposed change (after)

Action viewer collaborator member admin
View board, cards, columns, swimlanes
View movements & activities
View archived cards
Post comments
Delete own comments
Upload attachments
Delete attachments
Add / edit / delete checklist items
Create / edit / move / delete cards
Archive / unarchive cards
Columns / swimlanes / labels
Member management

viewer = read-only. collaborator = participate without managing cards.

Implementation note

comments, attachments, delete_attachment, and checklist/checklist_item actions currently call self._board() directly — they do not resolve the role. Each write path must be changed to call self._board_and_role() before the role check can be added. Do not introduce a new helper; reuse the existing pattern.

Tasks

Backend — permission checks

  • Change self._board()self._board_and_role() in comments, attachments, delete_attachment, checklist, checklist_item actions
  • POST comments/ — block viewer
  • DELETE comments/{id}/ — block viewer; collaborator may only delete own comments (check comment.author == request.user)
  • POST attachments/ — block viewer
  • DELETE attachments/{id}/ — block viewer
  • POST checklist/ — block viewer
  • PATCH checklist/{item}/ — block viewer
  • DELETE checklist/{item}/ — block viewer

Backend — tests (test_rbac.py)

  • viewer gets 403 on POST comments
  • viewer gets 403 on DELETE comments/{id}/
  • viewer gets 403 on POST attachments
  • viewer gets 403 on DELETE attachments/{id}/
  • viewer gets 403 on POST checklist
  • viewer gets 403 on PATCH checklist/{item}/
  • viewer gets 403 on DELETE checklist/{item}/
  • collaborator gets 201/200 on all seven endpoints above
  • collaborator cannot delete another user's comment (403)

Frontend

  • Update role picker UI (board member invite modal, group member management) to show all four roles with descriptions:
    • Admin — full board control
    • Member — can create and edit cards
    • Collaborator — can comment and upload files
    • Viewer — read-only

Docs

  • docs/features/rbac/roles.md — verify permission table matches the new matrix; update attachment/checklist/comment-delete rows
  • docs/api/cards.md — add minimum role requirement to: POST attachments, DELETE attachments, POST checklist, PATCH checklist/{item}/, DELETE checklist/{item}/ (POST comments already documented correctly)
  • CHANGELOG.md — add entry under ### Fixed: "enforce viewer read-only boundary on comments, attachments, and checklist endpoints; collaborator role now has distinct participatory write access"

Deferred (follow-up issues)

  • comment-delete-rbac — whether collaborator can delete any comment vs own-only is in scope above; full audit of edit-comment if that endpoint exists is deferred
  • export-import-rbacPOST /boards/import/ has no documented minimum role; should be member+ → separate issue

Migration

No data migration required. Existing viewer memberships lose comment/attachment/checklist write access. This is a tightening that closes a gap between the documented and actual behavior. Document in changelog with an upgrade note.

Acceptance criteria

  • viewer receives 403 on all write endpoints listed above
  • collaborator receives 201/200 on all the same endpoints
  • collaborator cannot delete another user's comment
  • All nine new test cases pass in test_rbac.py
  • docs/features/rbac/roles.md permission table is accurate
  • docs/api/cards.md lists minimum role on all affected endpoints
  • Changelog entry present under ### Fixed
Edited by Kelly Hair