Auth & RBAC
One-line: three layers work together on every request — who are you (JWT), which school (tenant), what may you do (RBAC + plan gates).
Authentication (JWT)
| Token | Lifetime | Stored | Use |
|---|---|---|---|
| Access token | 15 min | memory / SecureStore | Authorization: Bearer on every request |
| Refresh token | 30 days | HTTP-only cookie / SecureStorage | mint new access tokens via POST /api/auth/refresh (X-Refresh-Token header) |
Login (POST /api/auth/login) returns the access token plus the user, their permissions, and the tenant’s enabled_features — so a client knows what to show immediately. Passwords are hashed with bcrypt (cost ≥ 12). Auth endpoints are rate-limited (5 failed logins/min/account; lockout after 10 consecutive failures).
Account-status gates, checked before any token is issued:
- Soft-deleted users are invisible to lookup → behaves as “not found” →
401. - Suspended users →
403 AccountSuspended.
The login/profile response also carries role/setup flags: is_platform_admin, is_subadmin, is_setup_complete, allowed_unit_ids (branch scope), and force_password_reset.
Tenancy
Every /api/* request (except health, OPTIONS, platform routes) resolves a tenant in core/tenant.py, in priority order: body tenant_id → body subdomain → X-Tenant-ID → X-Tenant-Subdomain → Host subdomain → dev-fallback env var. A suspended tenant → 403; unknown → 404. Details in Multi-tenancy.
Authorization (RBAC)
Data model: User → UserRole → Role → RolePermission → Permission.
- A user can hold multiple roles; a role has multiple permissions.
- Permissions are global (seeded once, same strings everywhere); UserRole and RolePermission are tenant-scoped.
- Permission strings are
resource.action— e.g.student.read,attendance.mark,finance.manage,timetable.manage,rbac.manage.
Enforced by decorators, stacked in order:
@auth_required
@tenant_required
@require_permission('student.read')
def list_students(): ...Frontends mirror this with hasPermission() / <PermissionGate> (admin-web) and tab-visibility filters (mobile) — but the server is the source of truth; UI checks are cosmetic.
Seeded roles
server/scripts/seed_rbac.py seeds per tenant: School Admin (all), Teacher, Student, Finance Staff, Class Teacher. Fully customisable per tenant.
Plan feature gates
Orthogonal to RBAC: even with permission, a module is unavailable unless the tenant’s plan enables it. Keys live in core/plan_features.py (attendance, fees_management, notifications, schedule_management, student_management, teacher_management, class_management, timetable, transport, hostel) and are stored per plan in plans.features_json. Gate: @require_plan_feature('transport'). See Subscriptions & Plans.
Sub-admins (delegated tenant admins)
A School Admin can create sub-admins — tenant users with an explicitly-chosen subset of module permissions, backed by a private is_subadmin role (server/modules/sub_admins/). Delete-sensitive modules (students, teachers, finance) grant granular toggles rather than .manage (which would imply .delete).
Branch scoping: a sub-admin can be restricted to specific school_units. Enforcement (core/branch_scope.py) anchors on Class — Class.school_unit_id is the only unit-bearing column; students, attendance, fees, timetable reach a unit through a class. Restricted users fail closed (a classless student is invisible; tenant-wide structural ops like promote/copy are denied). Unrestricted users hit strict no-ops, so their behaviour is unchanged.
Passwords: reset, force-reset, change
- Forgot/reset (
/api/auth/password/forgot→/reset) — emailed token, unauthenticated, rate-limited, no account enumeration. Web links needADMIN_WEB_BASE_URLset in prod, else they point atlocalhost. - Force reset — sub-admins are provisioned with
force_password_reset=True; the flag is surfaced on login/profile so clients route them into a set-password screen. (In v1 the gate is enforced client-side.) - Self-serve change (
/api/auth/password/change) — authenticated, optional revoke of other sessions.
Strength rule everywhere: ≥ 8 chars and ≥ 1 digit. Password change/reset revokes sessions.