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

4.3 KiB

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

{
  "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.