MotorSignal-App/website/docs/backend-contracts/club-rank-permissions.md

99 lines
4.3 KiB
Markdown

# Backend Contract Proposal — Club Rank Permission Matrix (WEB-W1)
Status: **Proposed / not implemented.** This documents the backend work required
before the "Operational Permission Matrix" on
`app/clubs/[clubId]/manage/ranks-roles/page.tsx` can be made editable. Until this
ships, the matrix is rendered read-only (`persistenceAvailable={false}`).
## Problem
`handleSavePermissions` previously waited ~400 ms and showed a "Saved" state
without writing anything (confirmed in source prior to this change). There is no
Firestore document, API route, or security rule that stores or enforces a
per-rank capability matrix. The enforced club authorization model is **role
based**, derived solely from the administrative role on the member document:
- `lib/club-data.ts` → `getPortalPermissions(role)` maps `owner`/`admin` →
`canManageSettings`, `canInviteMembers`, `canApproveApplications`,
`canPlanRides`; `member` → all false.
- Display rank (`President`, `Road Captain`, …) is cosmetic and explicitly does
**not** grant software authority.
Persisting the matrix without enforcement would mislead club owners into
believing a toggle restricts members when it does nothing. That is prohibited by
the project rules (no unenforced permission models, no false success).
## Why this was not implemented in the website task
- Enforcement requires **Firestore security rule** changes (out of scope / frozen).
- A new persistence model must not conflict with the existing Flutter club role
model (`owner`/`admin`/`member`). Rank → capability is a different axis and
needs a product decision on precedence.
## Proposed contract (for a future, authorized backend change)
### 1. Storage
Document: `clubs/{clubId}/settings/rankPermissions`
```jsonc
{
"version": 1,
"updatedAt": "<serverTimestamp>",
"updatedByUid": "<uid>",
"ranks": {
"road_captain": {
"planRides": true,
"manageApplications": false,
"postAnnouncements": true,
"moderateWall": true,
"manageRoster": false,
"editProfile": false
}
// ... one entry per rank id in DEFAULT_CLUB_RANKS except `president`
}
}
```
Invariants:
- `president` is immutable (always full authority); never written or editable.
- Unknown rank ids and unknown capability keys are rejected.
- Capability keys are constrained to `DEFAULT_CLUB_CAPABILITIES` ids.
### 2. Security rules (requires authorized change to the frozen rules)
- Read: any active club member.
- Write: only `clubs/{clubId}.ownerUid == request.auth.uid`, or an active member
whose `role == 'admin'`. Validate the written shape (version, allowed rank ids,
boolean capability values only). Reject writes that would alter `president`.
### 3. Enforcement precedence (product decision required)
Rank capabilities may only **narrow**, never widen, what the administrative role
already denies. Administrative `role` remains the hard security boundary enforced
in rules; rank capabilities are a soft, owner-configurable delegation layer that
the Flutter and web clients honor for UI affordances. The Flutter club model must
adopt the same precedence so web and mobile agree.
### 4. Website wiring once the above exists
1. Add `fetchRankPermissions(clubId)` and `saveRankPermissions(clubId, callerUid, matrix)`
to `lib/club-data.ts`, mirroring the authorization pattern of
`updateClubAccessSettings` (owner/admin check + `updateDoc`).
2. In `ranks-roles/page.tsx`: load persisted permissions in `loadData`, and
implement `handleSavePermissions` to call `saveRankPermissions`, then re-read
to confirm the write survived before showing success.
3. Flip `<ClubPermissionMatrix persistenceAvailable={true} />` and pass
`onSavePermissions` / `onResetDefaults` / `isSaving`.
4. Add tests: persistence round-trip (save → reload → values survive), owner/admin
authorization success, member/unauthenticated rejection, `president`
immutability, and rejection of unknown rank/capability keys.
## Current truthful behavior (shipped in this branch)
- Matrix renders as a read-only reference of the standard governance model.
- No Save/Reset controls; a "View-only reference" indicator and an availability
notice explain that access is enforced by Owner/Admin roles and that per-rank
customization is planned.
- No save is attempted and no success is reported.