The Roles & Permissions subsystem is the BRM-owned layer that sits on top of WordPress roles. WordPress remains the canonical source of role definitions and capability maps, while BricksMembers owns the capability registry, role metadata, snapshots, bulk assignment flows, and the dedicated admin surface at admin.php?page=brm_roles.
Primary Boundaries
- WordPress core owns the actual role definitions and stores them in
wp_user_roles - BRM capability registry owns the
brm_*capability catalog and route-to-capability mapping - BRM role metadata owns non-core per-role flags such as admin lockout, descriptions, and notes
- BRM snapshot/export/import owns point-in-time copies of role definitions, not user assignments
- Roles & Permissions admin UI is a view/controller surface only; writes delegate into services
Canonical Owners
src/Services/Roles/RoleCapabilityRegistry.php— canonical BRM capability slugs, labels, groups, and surface mappingsrc/Services/Roles/RoleReadService.php— canonical read boundary for all registered-role reads and select optionssrc/Services/Roles/RoleWriteService.php— create, clone, update, delete, and rename slug flowssrc/Services/Roles/RoleAssignmentService.php— single-user and bulk user-to-role assignmentsrc/Services/Roles/RoleUsageService.php— role usage scanning and replacement guidance before deletionsrc/Services/Roles/RoleMetadataStore.php— BRM-owned per-role metadata such asblock_wp_adminsrc/Services/Roles/RoleSnapshotService.php— snapshots, export, and importsrc/Services/Roles/RoleCapabilityGrantService.php— administrator capability back-fill when BRM adds new capability slugs
Admin Entry Points
- Admin route:
src/Admin/Pages/Routes/RolesPermissionsPage.php - Template:
src/Admin/Pages/Templates/contract/roles.php - Client runtime:
assets/admin/js/roles-permissions-page.js - AJAX controller:
src/Ajax/RolesPermissionsActions.php - Capability gate:
RoleCapabilityRegistry::CAP_MANAGE_ROLES
The page localizes a single runtime payload into window.brmRolesPermissionsPage with current tab state, role rows, capability groups, snapshots, select options, and localized strings.
Storage and Option Contract
wp_user_roles— WordPress-owned canonical role definitionsbrm_role_metadata— BRM-owned per-role metadata keyed by role slugbrm_role_snapshots— BRM-owned snapshot/export/import storage, capped to recent entriesbrm_capability_grant_version— one-time back-fill marker for administrator BRM capabilities
Do not create a parallel BRM roles table. The accepted contract is that WordPress stays canonical and BRM layers only own the extra role metadata and capability abstractions it needs.
Bootstrap and Request Flow
bricksmembers.phpboots the pluginsrc/Core/Plugin.phpregisters hooks and the roles AJAX handlers- On
admin_initpriority 5, BRM back-fills administrator BRM capabilities and migrates any legacy admin-lockout state RolesPermissionsPage::render()builds the view model from the read, usage, metadata, snapshot, and capability services- The JS runtime renders the Roles and Users tabs from localized data and posts mutations to
brm_roles_*AJAX actions
AJAX Surface
The thin AJAX layer is RolesPermissionsActions. Current actions include:
brm_roles_createbrm_roles_clonebrm_roles_updatebrm_roles_deletebrm_roles_rename_slugbrm_roles_set_admin_lockoutbrm_roles_describe_usagebrm_roles_assign_userandbrm_roles_assign_usersbrm_roles_list_usersbrm_roles_capture_snapshot,brm_roles_list_snapshots,brm_roles_restore_snapshot, andbrm_roles_delete_snapshotbrm_roles_exportandbrm_roles_import
Read and Write Rules
- Do not call
wp_roles(),get_editable_roles(), orcount_users()directly outsidesrc/Services/Roles/ - Use
RoleReadServicefor all role lists, sanitization, protected-role checks, and select options - Use
RoleWriteServicefor definition changes; do not introduce a second mutation path - Use
RoleAssignmentServicefor user-role changes; do not modify assignments ad hoc in page code - Use
RoleUsageServicebefore deletion so references and user replacements are handled explicitly - Route permission checks through
Security::current_user_can()with aRoleCapabilityRegistryconstant, not a hardcodedmanage_optionsstring
Deletion and Replacement Contract
Deleting a role is not a raw remove_role() call. BRM first scans usage, requires replacement where needed, migrates references, and then removes the role through the write service. This protects user assignments and BRM-owned role-linked settings from being orphaned.
Administrator Grant Contract
Whenever BRM introduces a new brm_* capability, the registry change must be paired with a grant-version bump in RoleCapabilityGrantService. That one-time versioned pass ensures the Administrator role receives the new BRM capability without rerunning the grant on every admin page load.
Edit Guidance
- Start in
RoleCapabilityRegistrywhen adding or renaming BRM capabilities - Start in
RoleReadServicewhen changing how roles are read, normalized, or sanitized - Start in
RoleWriteServicefor definition writes, cloning, deletion, or slug changes - Start in
RoleAssignmentServicefor single-user or bulk assignment behavior - Start in
RoleMetadataStorefor admin lockout, descriptions, or notes - Start in
RoleSnapshotServicefor snapshot/export/import behavior - Keep
RolesPermissionsActionsthin; it should delegate, not own business logic
For the repository-grounded architecture view, use docs/feature-maps/roles-permissions.md. For the decision record behind the current model, use docs/adr/0005-roles-and-permissions.md.