Guide

WordPress "Sorry, you are not allowed to do that" — diagnosing capability check failures

Your editor is on Slack, frustrated. They opened /wp-admin/post.php?post=4912&action=edit, hit Update, and got a grey page with one line: "Sorry, you are not allowed to do that." They're still logged in.

1. Problem

Your editor is on Slack, frustrated. They opened /wp-admin/post.php?post=4912&action=edit, hit Update, and got a grey page with one line: "Sorry, you are not allowed to do that." They're still logged in. Their avatar is in the top-right corner. The role column in /wp-admin/users.php still says Editor. But every save fails. The block editor sidebar shows a red "Updating failed" toast. Their REST client gets {"code":"rest_cannot_edit","message":"Sorry, you are not allowed to edit this post.","data":{"status":403}}. Yesterday this worked. Today it doesn't. Nobody touched their account.

If you're searching for "wordpress sorry you are not allowed to do that", "wordpress rest api 403 capability lost", or "wordpress users locked out of admin pages after plugin update", you've landed on the right failure mode. This usually surfaces as a wp_rest_errors_total spike with error_code=rest_cannot_edit and status=403 — and the spike starts within minutes of a plugin update or a security-plugin "role lockdown" toggle, not when the user did anything wrong.

The dashboard does not tell you this. WordPress doesn't log capability denials anywhere by default. The user role looks correct. current_user_can('edit_posts') returns false for the user, but you'd never know unless you opened a debugger. The only artifact is the 403 response — and unless you're capturing REST traffic by status code, that 403 is invisible to every uptime monitor you own.

2. Impact

The cost of capability_lost is paid by the people who run the site, not the people who break it.

  • Editorial standstill. A typical mid-sized WordPress newsroom has 8–15 editors. When current_user_can('edit_post') flips to false for the Editor role, every scheduled post fails to publish, every "Update" button silently 403s, and the queue stalls. We've seen 4-hour outages where an entire daily edition missed the morning send because nobody realized "Sorry, you are not allowed" was role-wide.
  • WooCommerce shop_manager paralysis. When a security plugin strips manage_woocommerce from shop_manager, order edits, refunds, and stock updates all 403. Customers get charged but orders sit in pending forever — Logystera has logged single sites with 230+ stalled orders before anyone noticed.
  • REST integrations break silently. Your mobile app, your headless frontend, your Zapier workflows — anything authenticating as a non-admin service account starts failing with 403 against /wp/v2/posts, /wc/v3/orders, or /wp/v2/users/me. The integration logs an error; nobody reads it.
  • Trust damage with senior staff. When the Managing Editor can't save a post and the engineer's first response is "but your role is correct," the engineer loses credibility before the bug is even found.

A typical scenario: a WordPress agency pushes a plugin update at 14:00. By 14:15, three editors have pinged the help desk. By 15:00, the help desk has restarted PHP-FPM, cleared object cache, and opened a hosting ticket. The actual cause — an add_role migration in the new plugin version that overwrote the Editor capability map — won't be found until 17:30, after dozens of articles missed publication.

3. Why It’s Hard to Spot

The WordPress capability system is quiet on failure. There is no error_log line when current_user_can() returns false. The WP_REST_Server returns a generic 403 with code: rest_forbidden or code: rest_cannot_edit — and the 403 looks identical whether the user genuinely lacks the capability, the nonce is stale, or someone hit a protected endpoint by mistake.

The role name on the user object never changes. wp_users.user_login still maps to wp_usermeta.wp_capabilities = a:1:{s:6:"editor";b:1;}. What changes is the wp_options row keyed wp_user_roles, which holds the capability map for each role. When a plugin calls $wp_roles->remove_cap('editor', 'edit_others_posts') during activation or upgrade, that single serialized blob mutates — and every Editor immediately loses that capability. The user table looks fine. The role list looks fine. The capability is just gone.

Uptime monitors hit / or /wp-login.php and get a 200. The site is "up." Front-end visitors see no problem. Only authenticated requests to capability-gated endpoints fail, and only for roles that lost a cap — which means your monitoring sees a healthy site while half your editorial team can't work. This is the canonical silent failure: a status-code spike inside an authenticated tier of traffic that no public probe will ever touch.

4. Cause

The Logystera processor emits wp_rest_errors_total from every WordPress site running the plugin. The metric is incremented when an http.request event arrives with path matching /wp-json/* AND status in (401, 403, 404, 500). Labels include error_code (the WP REST error string), route_namespace, method, and entity_id.

When a capability check fails inside WP_REST_Server::dispatch(), WordPress returns a WP_Error with code rest_cannot_edit, rest_cannot_delete, rest_cannot_create, rest_forbidden, or — depending on the endpoint — a custom string like woocommerce_rest_cannot_edit. The plugin captures the response code and the WP error code from the response body and tags the signal. So a wp_rest_errors_total{error_code="rest_cannot_edit",status="403"} increment is the literal mechanical fingerprint of current_user_can() returning false during a REST request.

The same root cause also drives wp_admin_requests_total{status="403"} for non-REST /wp-admin/* POSTs (e.g. post.php, users.php, options.php), because admin-ajax and admin-post.php run the same capability checks. Watch both metrics together and you can tell whether the breakage hit the REST surface only or the entire admin layer.

5. Solution

5.1 Diagnose

Start at the WordPress side. The plugin records every REST 403 as an http.request envelope and the processor materializes it as wp_rest_errors_total. On disk, the equivalents live in the access log and the database.

Find the capability_lost spike in nginx/Apache access logs. Authenticated REST requests carry the WP cookie:

# All REST 403s in the last hour, by route
awk '$9 == 403 && $7 ~ /\/wp-json\//' /var/log/nginx/access.log \
  | awk -v d="$(date -d '1 hour ago' '+%d/%b/%Y:%H')" '$4 >= "["d' \
  | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
# → surfaces wp_rest_errors_total bucketed by route

Pull the actual error code from the response. PHP-FPM doesn't log response bodies, so capture them at the app layer. If the WP plugin's debug mode is on, the audit log records every WP_Error:

tail -F /wp-content/debug.log | grep -E "rest_cannot_edit|rest_forbidden|rest_cannot_delete"
# → each line is a wp_rest_errors_total increment with error_code visible

Time-correlate against plugin activity. This is the diagnostic step that turns a 403 spike into a story:

# When did wp_options:wp_user_roles last change? (capability blob)
wp option pluck wp_user_roles editor capabilities --format=json
wp db query "SELECT option_name, FROM_UNIXTIME(UNIX_TIMESTAMP()) FROM wp_options \
  WHERE option_name = 'wp_user_roles'"
# Then cross-reference with plugin updates:
wp plugin list --update=available --format=json
grep -E "activated_plugin|upgrader_process_complete" /wp-content/debug.log \
  | tail -50
# → ties wp_state_changes_total (plugin update at 14:03) to wp_rest_errors_total spike at 14:05

Identify the affected capability for a specific user. This is the one query that resolves "why can this Editor not edit?":

# What does WP think this user can do right now?
wp eval 'print_r(get_userdata(42)->allcaps);' | grep -E "edit_|publish_|delete_"

# Compare against Editor role's current capability map:
wp eval 'print_r(wp_roles()->roles["editor"]["capabilities"]);' \
  | grep -E "edit_others_posts|publish_pages|edit_published_pages"
# → if 'edit_others_posts' shows false in role but true in user, role got stripped

Cross-reference REST errors with admin POST 403s to confirm scope:

# Admin-side 403 spike (wp_admin_requests_total) in same window
awk '$9 == 403 && $7 ~ /\/wp-admin\/(post|users|options)\.php/' \
  /var/log/nginx/access.log | tail -50
# → if both wp_rest_errors_total AND wp_admin_requests_total spiked together,
#   the capability map mutated; if only REST spiked, suspect REST-specific filter

5.2 Root Causes

Three patterns produce 99% of capability_lost incidents. Each maps to a distinct signal fingerprint.

  • Plugin programmatically stripped capabilities (most common). A plugin's activation hook, upgrade routine, or admin-side "cleanup" tool calls $wp_roles->remove_cap() or rewrites wp_user_roles directly. Fires wp_rest_errors_total{error_code="rest_cannot_edit"} for every affected user, plus wp_state_changes_total{change_type="role_capability"} at the moment of mutation. Look for a single sharp spike that starts immediately after a wp_state_changes_total{change_type="plugin_updated"} event.
  • Security plugin role-protection feature triggered (false positive). Wordfence, iThemes Security, Solid Security, or Patchstack flag a non-admin role as "elevated" and quietly strip capabilities they consider dangerous (edit_users, manage_options, unfiltered_html). This often fires on a schedule, not on user action. Surfaces as wp_rest_errors_total{error_code="rest_forbidden"} and wp_admin_requests_total{status="403"} correlated with wp.cron runs of wordfence_ or itsec_ hooks, often during low-traffic hours when nobody is watching.
  • Role schema migration after a user-role plugin update. Members, User Role Editor, PublishPress Capabilities, or WooCommerce Subscriptions ships a release that re-keys capabilities (e.g. read_private_shop_ordersread_private_orders). The user role still exists; the capability strings inside it changed. Fires wp_rest_errors_total clustered on plugin-specific endpoints (/wc/v3/orders, /wp/v2/users) rather than core ones, and only for the roles managed by that plugin.

5.3 Fix

Restore the lost capability. Order matters: stop the bleeding first, then find why.

  1. Restore caps for the affected role immediately. WP-CLI is the fastest path:
   # Restore Editor's standard capability set
   wp role reset editor
   # or, surgically:
   wp cap add editor edit_others_posts publish_pages edit_published_pages

For a custom role that wp role reset doesn't know:

   wp eval '
     $r = get_role("shop_manager");
     $r->add_cap("manage_woocommerce");
     $r->add_cap("edit_shop_orders");
     $r->add_cap("read_private_shop_orders");
   '
  1. For per-user capability overrides (a single user's wp_capabilities meta was mutated, not the role):
   wp user remove-cap 42 do_not_allow
   wp user add-cap 42 edit_others_posts
  1. Identify and disable the offending plugin. Match the timestamp of the last wp_options.wp_user_roles mutation to your plugin update history (wp plugin list --update=available plus your audit log). Disable the suspect plugin via WP-CLI to avoid hitting /wp-admin/plugins.php (which will 403 if activate_plugins got stripped):
   wp plugin deactivate wordfence
   # Confirm the role mutation stopped:
   wp option pluck wp_user_roles editor capabilities --format=json
  1. For role-schema migrations, run the plugin's compatibility script. PublishPress Capabilities ships pp-capabilities-resync; User Role Editor has a "Reset roles" button. If the plugin offers no migration path, dump the pre-update role from a recent backup (wp_options.wp_user_roles) and restore it via update_option('wp_user_roles', $old_blob).
  1. Hard-pin the capability map so the next bad upgrade can't repeat the silent strip. Add to wp-config.php or a must-use plugin:
   add_action('init', function() {
       $editor = get_role('editor');
       foreach (['edit_others_posts','publish_pages','edit_published_pages'] as $cap) {
           if (!$editor->has_cap($cap)) { $editor->add_cap($cap); }
       }
   }, 1);

5.4 Verify

After the fix, you need two things to stop and one thing to start.

  • wp_rest_errors_total{error_code="rest_cannot_edit",status="403"} should stop incrementing for the affected entity. Healthy baseline for a busy newsroom: 5–15 per hour (typos in REST clients, expired nonces, occasional stale tabs). If you were running at 200+/hour during the incident and it drops to under 20/hour within 15 minutes, you fixed it. If it stays at 50+/hour, you fixed one role but missed another — keep looking.
  • wp_admin_requests_total{status="403"} baseline returns to 0–5/hour per site. Anything sustained above 30/hour means the admin path is still gated.
  • wp_logged_in_requests_total{role="editor"} should rise as editors who'd been blocked retry their saves. If you fixed the cap and this metric does not recover within 30 minutes, your editors gave up and went home — which is its own kind of impact, but it confirms the fix from the user side.
  • Run a synthetic check: wp post update 4912 --post_title="verification" as the affected user via WP-CLI's --user= flag. Success on this one command means the cap is back. Failure means the role still lacks the cap — go back to 5.3 step 1.

If wp_rest_errors_total is at zero within 30 minutes, baseline is back to 0–15/hour, and synthetic post-update succeeds, the incident is resolved. Generic "the editor saved a post" is not enough — that proves one user works, not that the role is correct.

6. How to Catch This Early

Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.

This issue surfaces as wp_rest_errors_total.

Everything you just did manually — grep the access logs for 403s, correlate the spike with a plugin update timestamp, identify which capability went missing, distinguish a role-wide strip from a per-user mutation — Logystera does automatically. The same wp_rest_errors_total you just searched for is detected, charted, and alerted in real time, broken down by error_code, route_namespace, and entity_id, so the moment a Wordfence cron run strips edit_others_posts from your Editor role at 03:14, you get a notification with the log line that proves it.

!Logystera dashboard — wp_rest_errors_total over time

wp_rest_errors_total spike at 14:03 with error_code=rest_cannot_edit, immediately after a security plugin auto-update. Last 24h, grouped by error code.

!Logystera alert — REST capability check failures detected

Critical alert fires within 60s of a 10x spike in wp_rest_errors_total{error_code=rest_cannot_edit}, with the offending plugin update timestamp pinned to the same evidence panel.

The fix is simple once you know the problem. The hard part is knowing it happened at all — particularly when the only people seeing the failure are senior editors and shop managers who assume their saves worked because the page didn't show a red banner. Logystera turns capability_lost from a Slack ping at 16:30 ("hey, can someone look at why none of my updates are saving?") into a 60-second notification at 14:04, with the WP error code, the role affected, and the plugin update that triggered it already attached.

7. Related Silent Failures

These cluster around the same wp_rest_errors_total and wp_state_changes_total signals. If capability_lost hit you, one of these is probably next.

  • REST 401 / nonce expiration cascadeswp_rest_errors_total{error_code="rest_cookie_invalid_nonce"}. Looks like capability_lost but is a session/nonce issue. Same dashboard, different error code.
  • Plugin silent activation / deactivationwp_state_changes_total{change_type="plugin_activated"}. The change that caused the capability strip. Watch this signal as the leading indicator for capability_lost.
  • Privilege escalation / role added without admin actionwp_state_changes_total{change_type="user_role_added"}. The mirror image: someone gained a capability they shouldn't have. Same telemetry pipeline, opposite security implication.
  • WooCommerce shop_manager order-edit failureswp_rest_errors_total{error_code="woocommerce_rest_cannot_edit",route_namespace="wc/v3"}. Specific to WC capability schema migrations.
  • wp-admin POST 403 spikes during low-traffic hourswp_admin_requests_total{status="403"} correlated with wp.cron runs of security-plugin hooks. The "happens at 3am, nobody notices until morning" failure.

See what's actually happening in your WordPress system

Connect your site. Logystera starts monitoring within minutes.

Logystera Logystera
Monitoring for WordPress and Drupal sites. Install a plugin or module to catch silent failures — cron stalls, failed emails, login attacks, PHP errors — before users report them.
Company
Copyright © 2026 Logystera. All rights reserved.