Skip to content

0006 — Two-plane RBAC with reconciled role model ​

Status: Accepted — extended by ADR 0023 (2026-06-18: adds comms_author) and extended by ADR 0037 (2026-06-23: adds infra_admin at level 7, promotes ministry_leader to level 6, adds media_steward, and adds five homeschool portal roles). The two-plane model and core server-side enforcement principle are unchanged.

Date: 2026-06-17

ADO work item: AB#3073

Deciders: Kristopher Turner (platform owner)


Context ​

The platform has two distinct authorization concerns that must never be conflated:

  • Plane 1 — Infrastructure access: operators and CI/CD systems touching Azure, GitHub, ADO, and Key Vault. This is where Microsoft Entra is used. Governed by Azure RBAC + Entra roles + managed identities with least-privilege assignments.
  • Plane 2 — Application authorization: community members accessing platform features based on their ministry role. This has nothing to do with Entra.

A known inconsistency existed between database/schema.sql (four permission levels) and docs/internal/overview/ministry-leadership-overview.md / README (five Main-Hub roles). This ADR reconciles them into one canonical model.

Decision ​

Plane 1 (infrastructure): Azure RBAC + Entra. Only platform operators and CI/CD service principals are Entra identities. No community member ever holds an Entra role.

Plane 2 (application): canonical roles enforced server-side in the API only (five at acceptance; a sixth, comms_author, added by ADR 0023 — see the role table). The client never sends a role claim that the server trusts. A Clerk JWT sub claim maps to a Users row; the server reads Users.role from the database for every authorization check.

Canonical role model (Plane 2) ​

Updated by ADR 0037 (2026-06-23). The table below reflects the complete role set after that expansion. See ADR 0037 for full rationale, migration details, and authorization middleware consequences.

Roles are divided into two types:

  • Ordinal roles carry a numeric permission level. requireRole(minLevel) checks pass for any user whose level is greater than or equal to minLevel.
  • Feature roles carry permissionLevel 0. They are treated as member-level in ordinal checks. Use requireAnyRole() for feature-specific endpoint guards.
RoleSlugLevelTypeDescription
Infrastructure Admininfra_admin7ordinalAdded by ADR 0037. Infrastructure and platform operators. God-mode access. Not for community administration.
Ministry Leaderministry_leader6ordinalMinister. Level promoted from 4 → 6 by ADR 0037. Community spiritual and ministry authority; passes all admin-level checks.
Community Adminadmin5ordinalDay-to-day community administration; member approval authority; all community configuration.
(reserved)4Level 4 is reserved for future use.
Small Group Leadergroup_leader3ordinalManages their assigned small group.
Membermember2ordinalStandard access; family management (with approval).
Visitor / Prospectivevisitor1ordinalApproval-gated; minimal read access only.
Sermon and Music Managermedia_stewardfeatureAdded by ADR 0037. Manages worship media uploads, music library, and sermon content. Scoped to the Sermons and Music portal.
Communications Authorcomms_authorfeatureAdded by ADR 0023 (2026-06-18). Drafts Announcements and submits to the approval queue. Cannot approve, reject, or publish. Scoped to Announcements only.
Homeschool Administratorhomeschool_adminfeatureAdded by ADR 0037. Administrates the homeschool program. Capabilities TBD when homeschool portal is built.
Homeschool Teacherhomeschool_teacherfeatureAdded by ADR 0037. Teaches classes in the homeschool program. Capabilities TBD.
Homeschool Advisorhomeschool_advisorfeatureAdded by ADR 0037. Advises and counsels students and families. Capabilities TBD.
High School Studenthighschool_studentfeatureAdded by ADR 0037. Enrolled in the once-weekly high school program. Capabilities TBD.
Homeschool Studenthomeschool_studentfeatureAdded by ADR 0037. Base homeschool portal access. Capabilities TBD.

The model is thirteen Plane-2 roles as of 2026-06-23 (six ordinal, seven feature). Children are a sub-account type (see ADR 0007), not a separate role — they inherit a restricted scope of the member role as defined by their parent's approval settings.

How a social-login subject maps to an app user + role ​

  1. Clerk validates the Apple/Google token and issues a session JWT containing the provider sub claim.
  2. On first sign-in, the API creates a Users row with credential_type = 'social', role = 'visitor', and status = 'pending_approval'.
  3. A minister reviews and approves the user — the API updates Users.role and Users.status.
  4. On subsequent API calls, the server reads Users.role from the database. The JWT's sub is the only trusted claim from the client; the role is never trusted from the client.

Alternatives considered ​

OptionProsConsWhy not chosen
Five roles, server-side enforcement (chosen)Matches the ministry's actual structure; Visitor role enables approval-gated onboardingRequires schema update (four → five roles)— chosen
Four roles (schema as-is, no Visitor)No schema changeLoses the ability to represent prospective members in the DB before approvalApproval workflow requires a Visitor/pending state
ABAC (attribute-based)Fine-grained; composableSignificantly more complex to implement and reason aboutNot justified for a community of ~200 with well-defined roles
Client-asserted roles (JWT claims only)Less DB lookupsFundamental security anti-pattern — clients cannot be trustedHard security requirement

Consequences ​

Positive ​

  • Clean separation: Entra never appears in community member auth flows; application roles never appear in Azure RBAC.
  • Server-side enforcement means a compromised client cannot escalate privileges.
  • The five-role model matches the ministry leadership structure as documented — no translation layer needed.
  • The Visitor role enables a clean approval-gated onboarding flow without special-casing.

Negative / trade-offs ​

  • database/schema.sql must be updated: four permission levels → five canonical roles. This is a Phase 2 migration. (The Role enum was further extended by ADR 0037 via Prisma migration 20260623000000_rbac_role_expansion.)
  • Every API endpoint must perform a Users.role lookup (or cache it in the session). Mitigation: include role in the server-side session object after auth; do not re-query on every request.

Risks ​

  • Role escalation via DB — if a row in Users can be updated by a non-admin, privilege escalation is possible. Mitigation: role updates are admin-only API operations; no endpoint allows a member to self-modify their role.
  • Child scope creep — children's access must be explicitly bounded by parent approval, not just by role. The application layer must enforce per-section access controls on top of the role check for member-scoped child sub-accounts.

References ​

Heritage Community Hub — Internal. Access restricted via Cloudflare Access + Entra ID.