Search our AI Skills
WordPress Role-Based Access
WordPress Role-Based Access
Instructions
Design and implement fine-grained access control in WordPress using custom roles, capabilities, and permission-scoped REST API endpoints. WordPress’s built-in role system (Administrator, Editor, Author, Contributor, Subscriber) is insufficient for multi-user applications that require data isolation and granular permissions.
Custom Role Architecture
Define custom roles that map to your application’s user types:
function register_custom_roles() {
add_role('household_owner', 'Household Owner', [
'read' => true,
'manage_household' => true,
'view_all_members' => true,
'manage_tasks' => true,
'manage_budgets' => true,
'invite_members' => true,
'remove_members' => true,
'view_analytics' => true,
]);
add_role('household_member', 'Household Member', [
'read' => true,
'view_own_tasks' => true,
'manage_own_tasks' => true,
'view_shared_budgets' => true,
'view_household_calendar' => true,
]);
add_role('household_child', 'Household Child', [
'read' => true,
'view_own_tasks' => true,
'complete_own_tasks' => true,
]);
}
register_activation_hook(__FILE__, 'register_custom_roles');
Design principles:
-
Least privilege: Each role gets only the capabilities it needs. Never grant
manage_optionsoredit_others_poststo custom roles - Hierarchical inheritance: Build capability sets so higher roles are strict supersets of lower roles where appropriate
- Additive model: Start with zero capabilities and add. Never start with admin capabilities and subtract
-
Plugin-scoped: Prefix all custom capabilities with your plugin slug to avoid collisions (
myplugin_manage_tasks, notmanage_tasks)
Capability Mapping
Map every application action to a specific capability:
| Action | Capability | Roles |
|---|---|---|
| View own tasks | plugin_view_own_tasks |
owner, member, child |
| Create tasks for others | plugin_assign_tasks |
owner |
| View household budget | plugin_view_budgets |
owner, member |
| Edit budget entries | plugin_manage_budgets |
owner |
| Invite new household members | plugin_invite_members |
owner |
| View analytics dashboard | plugin_view_analytics |
owner |
| Export household data | plugin_export_data |
owner |
Enforce capabilities at every decision point:
if (!current_user_can('plugin_manage_tasks')) {
wp_die(__('You do not have permission to manage tasks.', 'my-plugin'));
}
Permission-Scoped REST API
Build REST endpoints that enforce capabilities and data isolation:
register_rest_route('myplugin/v1', '/tasks', [
'methods' => 'GET',
'callback' => 'get_tasks',
'permission_callback' => function () {
return current_user_can('plugin_view_own_tasks');
},
]);
Data isolation rules:
- Row-level security: Every database query must filter by the current user’s household/group. Never return data from other households
- Ownership verification: For update/delete operations, verify the requesting user owns or has authority over the target record
- Response filtering: Even if the database query returns extra fields, strip fields the current user’s role should not see before returning the response
function get_tasks($request) {
$user_id = get_current_user_id();
$household_id = get_user_meta($user_id, 'household_id', true);
if (!$household_id) {
return new WP_Error('no_household', 'User is not in a household', ['status' => 403]);
}
global $wpdb;
$table = $wpdb->prefix . 'plugin_tasks';
if (current_user_can('plugin_view_all_tasks')) {
$tasks = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table WHERE household_id = %d",
$household_id
));
} else {
$tasks = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table WHERE household_id = %d AND assigned_to = %d",
$household_id, $user_id
));
}
return rest_ensure_response($tasks);
}
Role Transparency Pattern
Users should understand their own permissions and the permissions of others in their group:
- My permissions page: Show users what they can and cannot do in plain language (not capability slugs)
- Role badge: Display role labels in the UI next to usernames (Owner, Member, Child)
- Permission denied messages: When a user attempts a forbidden action, explain what role/permission is required and who can grant it
- Role comparison: Allow owners to see a side-by-side comparison of what each role can do
- Audit log: Record permission-sensitive actions (role changes, member additions/removals, data exports) with actor, action, target, and timestamp
Dynamic Capability Grants
Support temporary or context-specific permission elevation:
- Temporary elevation: Owner can grant a member temporary access to a specific capability (e.g., budget editing for a week)
- Task-specific permissions: Some actions may be permitted only for records the user created or was assigned to, not for all records of that type
- Time-bounded access: Store grant expiration timestamps and enforce them on every capability check
function user_has_temporary_cap($user_id, $capability) {
$grants = get_user_meta($user_id, 'temporary_capabilities', true);
if (!is_array($grants) || !isset($grants[$capability])) {
return false;
}
return $grants[$capability]['expires'] > time();
}
Security Hardening
- Nonce on every mutation: All POST/PUT/DELETE endpoints verify a nonce
- Capability check on every endpoint: Never rely solely on authentication (logged in) — always check specific capabilities
- No capability escalation: A user cannot grant capabilities they do not themselves possess
- Role change audit: All role changes are logged and can trigger email notifications to affected users
- Deactivation cleanup: On plugin deactivation, decide whether to remove custom roles (clean) or preserve them (safe). Document the choice and expose a setting
Testing Role Isolation
- Multi-user test scenarios: Create test users for each role and verify access boundaries
- Cross-household isolation test: Verify that User A in Household 1 cannot access any data from Household 2
- Capability boundary test: For each capability, verify that removing it from a role correctly blocks the associated actions
- REST API fuzzing: Test each endpoint with tokens from different roles and verify appropriate 403 responses
Inputs Required
- Application user types and their intended access levels
- Data entities and which roles can read/write/delete each
- REST API endpoints that need permission scoping
- Multi-tenancy model (household, organization, or other grouping)
- Audit and compliance requirements
Output Format
- Custom role definitions with complete capability mappings
- Capability-to-action mapping table
- Permission-scoped REST API endpoint specifications
- Row-level security query patterns
- Role Transparency UI specifications
- Audit log schema
- Role isolation test plan
Anti-Patterns
-
Checking
is_admin()instead of capabilities:is_admin()checks the screen context, not the user’s role. Always usecurrent_user_can() - Single god-role: Creating one custom role with all permissions instead of granular role hierarchy
- No data isolation: Checking capabilities for actions but not filtering data queries by ownership/group
-
Hardcoded role names in business logic: Using
if ($role === 'household_owner')instead ofcurrent_user_can('plugin_manage_household')— this breaks when roles are renamed or capabilities reorganized -
Ignoring REST API permissions: Setting
permission_callback => '__return_true'on REST routes - Role cleanup on deactivation: Removing roles on deactivation destroys user assignments. Roles should persist unless the plugin is fully uninstalled
-
Capability name collisions: Using generic capability names (
manage_tasks) that could conflict with other plugins - No audit trail: Implementing access control without logging who did what
