Black Friday: 79% off every plugin Shop the sale →

Role Simulation and Safety

An audit calls each ability’s permission check as every role. This article explains how that is done without touching real users or running anything.

Simulated users#

  • No real users are touched. No user, role or capability is created, changed or deleted. A simulated user is swapped into the current-user global for each check, and the original user is restored afterwards, even if the callback throws.
  • Anonymous is a plain WP_User with ID 0, exactly like a logged-out visitor.
  • Logged-in roles use a sentinel user ID near the top of the integer range, one per role, that cannot belong to a real user. The user gets the role’s capabilities copied from the role definition.
  • No database writes. While a check runs, user-meta writes for the sentinel IDs are blocked.

Why not ID 0 for every role? Core’s is_user_logged_in() only checks that the user ID is not 0. If simulated subscribers had ID 0, an ability guarded only by is_user_logged_in() would wrongly look locked down. The sentinel ID makes those abilities show up correctly as runnable by any logged-in user.

Test input#

Permission callbacks often look at the input, so the auditor builds a realistic test input from the ability’s input_schema: the schema default first, then const, then the first enum value, then a placeholder that fits the type and format (email, URI, date-time, UUID, length limits, number bounds). Objects get their required properties plus any property with a default. The input goes through the same normalize and validate steps core’s REST runner uses before a permission check. The detail panel shows the input that was used.

Results#

ResultMeaning
PassThe check returned true.
DenyThe check returned false.
InconclusiveNo reliable answer.
n/aThe role does not exist on this site.

A check is Inconclusive when it throws, calls wp_die(), returns a WP_Error or a non-boolean, runs over its time budget, tries to execute an ability, denies because the test input failed validation, or denies every role including Administrator (which means it depends on the input rather than the role). The reason is shown on hover in the matrix and in full in the detail panel.

The execution guard#

The guard is armed for the whole audit:

  • On WordPress 7.1 and later, any execution attempt is recorded through wp_ability_invoked and replaced with an error through wp_pre_execute_ability, before the execute callback runs.
  • On WordPress 6.9 and 7.0, execution is stopped at wp_before_execute_ability, which runs after the permission check but before the execute callback.

Any attempt is listed as a warning on the Report tab and in WP-CLI output.

Time budgets#

Each permission check gets 2 seconds and the whole audit gets 60 seconds. PHP cannot stop a callback mid-run, so a slow check is reported as inconclusive after it finishes. Once the audit budget is used up, the remaining checks are skipped and marked inconclusive. Both budgets can be changed with filters; see Hooks & Filters.

Containment#

Output, PHP notices and wp_die() calls from third-party callbacks are captured so they cannot break the admin screen or the WP-CLI output.

Was this article helpful?