Skip to main content
< All Topics
Print

WordPress Role-Based Access

name: wordpress-role-based-access

description: Design and implement WordPress custom roles, capabilities, and permission-scoped REST APIs for multi-user applications. Granular data access control, capability mapping, Role Transparency pattern. Use when building multi-user WordPress plugins, designing custom role hierarchies, implementing capability-scoped REST endpoints, or applying the Role Transparency pattern.

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_options or edit_others_posts to 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, not manage_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 use current_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 of current_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
Table of Contents