critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

canAccess(route, role) is synchronous and returns bool without any async gap or network call
Permission matrix covers all defined routes and all defined UserRole values (peerMentor, coordinator, orgAdmin, globalAdmin)
peerMentor cannot access bulkRegister, exportBufdir, or attestExpense routes/actions
coordinator can access bulkRegister and attestExpense but not exportBufdir unless orgAdmin
globalAdmin canAccess returns false for all mobile routes (blocked at mobile layer)
checkPermission(action, context) is async and reads active role from RoleStateManager stream
The permission matrix is defined as a single const data structure โ€” not scattered if/else chains
canAccess does not throw for unknown routes โ€” returns false by default (deny-by-default policy)
Service is exposed as a Riverpod Provider
All five core actions (registerActivity, viewContacts, bulkRegister, exportBufdir, attestExpense) are enumerated in a PermissionAction enum

Technical Requirements

frameworks
Flutter
Riverpod
Dart
data models
assignment
activity
contact
performance requirements
canAccess must complete in <1ms โ€” pure in-memory map lookup, zero I/O
Permission matrix loaded once at app start, not re-fetched per call
security requirements
Deny-by-default: unknown route or role evaluates to false
Permission matrix must be immutable const โ€” no runtime mutation
RLS on Supabase remains the authoritative server-side enforcement; client-side check is UX-only guard
globalAdmin always denied on mobile routes regardless of matrix โ€” hard-coded override

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Model the matrix as: const Map> _allowedRoutes and const Map> _allowedActions. Use an extension method on UserRole for readability. canAccess(String route, UserRole role) simply does _allowedRoutes[role]?.contains(route) ?? false.

The async checkPermission reads activeRole from RoleStateManager using ref.read (not watch) since it is called imperatively. Document each matrix entry with a comment linking to the workshop requirements (e.g., bulkRegister: coordinator and above per NHF/HLF workshop requirement ยง2.4).

Testing Requirements

Unit tests with flutter_test: enumerate all UserRole ร— route/action combinations and assert expected bool from canAccess. Test deny-by-default for unknown route strings. Test checkPermission async path with mocked RoleStateManager stream. Verify globalAdmin override returns false even if matrix entry exists.

Test that the permission matrix const is exhaustive โ€” a test that lists all PermissionAction values and asserts each has an entry for each UserRole. Target 100% coverage of the permission matrix evaluation path.

Component
Permission Checker Service
service medium
Epic Risks (3)
high impact low prob security

A coordinator's permissions could be revoked by an admin while they are actively using the app. If the permission checker relies solely on the cached role state from login, the coordinator could continue performing actions they are no longer authorized for until the next login.

Mitigation & Contingency

Mitigation: The Permission Checker Service must re-validate against the Role Repository (not just in-memory state) before high-impact actions. Implement a configurable staleness window (e.g., 15 minutes) after which role data is refreshed from Supabase in the background.

Contingency: If a revoked permission is detected during a pre-action check, immediately clear the cached role state, force a re-resolution from Supabase, and display an inline error explaining the permission change rather than crashing or silently failing.

medium impact medium prob technical

Using both BLoC and Riverpod in the same state management layer for roles risks state synchronization bugs where one system updates before the other, causing widgets to render with stale role data during the switch transition.

Mitigation & Contingency

Mitigation: Choose a single primary state management approach (Riverpod StateNotifier is recommended) for role state and wrap the BLoC pattern within it if legacy code requires BLoC interfaces. Establish a single source-of-truth provider that all consumers read from.

Contingency: If synchronization bugs appear during integration testing, introduce a RoleStateReady gate widget that delays rendering of role-dependent UI until the state notifier emits a confirmed resolved state, preventing partial renders.

medium impact high prob scope

Hardcoded permission constants per role can become a maintenance burden as new features are added across 61 total features, leading to permission definitions that are scattered, stale, or inconsistent.

Mitigation & Contingency

Mitigation: Centralize all role-permission mappings in a single RolePermissions constants file with named action keys. Enforce that no widget or service directly checks role type strings; all checks must go through the Permission Checker Service.

Contingency: If permission definitions drift out of sync, introduce a validation test suite that cross-references all registered permission constants against their usage sites and fails the CI build if an undefined permission key is referenced.