Skip to content
This repository was archived by the owner on Jul 29, 2026. It is now read-only.

fix(admin): make governance capabilities readable on the user profile - #205

Merged
dknauss merged 3 commits into
mainfrom
claude/whats-next-dc6xeq
Jul 23, 2026
Merged

fix(admin): make governance capabilities readable on the user profile#205
dknauss merged 3 commits into
mainfrom
claude/whats-next-dc6xeq

Conversation

@dknauss

@dknauss dknauss commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • What changed? WP Sudo grants its four governance capabilities (manage_wp_sudo, view_wp_sudo_activity, export_wp_sudo_activity, revoke_wp_sudo_sessions) directly to users via WP_User::add_cap(). WordPress core's profile screen (wp-admin/user-edit.php) rendered those per-user caps as a run-on list of raw slugs under the generic "Additional Capabilities" heading. This adds two profile-screen callbacks in Admin::register():
    • suppress_governance_capabilities_display() on the core additional_capabilities_display filter — hides the raw section only when every non-role capability the profile user holds is a truthy governance cap. Any foreign additional cap → returns the incoming value unchanged (never hides other plugins' caps, never overrides an already-hidden section); a denied governance cap stays visible as core's "Denied: …". Non-role classification uses wp_roles()->is_role() to mirror core, so additional custom roles aren't mistaken for stray caps.
    • render_governance_capabilities_profile_note() on show_user_profile + edit_user_profile — renders a readable "Sudo capabilities" block listing each directly granted capability by its get_cap_label() label, reading raw stored grants (not effective checks) so a multisite super admin isn't misreported. The Access-tab link is shown only to a viewer who can reach it (wp_sudo_can('manage_wp_sudo')); others see plain text.
  • Why was this change needed? The raw slug list was unattractive and not self-explanatory to site admins — the sibling to the 4.5.0 Access-tab readability fix (fix(admin): make Access-tab capability table readable and slug-free #144), now applied to the profile page. Display-only: capability enforcement is untouched.

Validation

  • composer test — 1,040 unit tests / 3,129 assertions pass (12 new tests covering the hide / pass-through / denied / custom-role / no-cap suppression branches and the render branches with/without the manager link and partial grants)
  • composer lint (PHPCS) clean on production; composer analyse (PHPStan + Psalm) clean
  • Manual checks described: a live profile.php render was not exercised — an integration/browser stack (MySQL) isn't provisioned in this environment, and the change is display-only with full unit coverage of both callbacks. The WordPress core filter and its count(caps) > count(roles) && true === $display render condition were verified against wp-admin/user-edit.php on WordPress trunk.

Checklist

  • Linked issue or explained why none was needed — no tracked issue; surfaced from maintainer feedback that the profile "Additional Capabilities" section is unattractive/hard to understand
  • Updated docs, tests, or manual testing guidance if behavior changed — added a 4.8.0 CHANGELOG entry and refreshed unit-test/size counts in docs/current-metrics.md
  • No sensitive details disclosed publicly

Generated by Claude Code

WP Sudo grants its four governance capabilities directly to users via
WP_User::add_cap() (activation, upgrade, the Access-tab grant handler).
WordPress core's profile screen (wp-admin/user-edit.php) rendered those
per-user caps as a run-on list of raw slugs under a generic "Additional
Capabilities" heading — unattractive and unexplained.

Add two profile-screen callbacks in Admin::register():

- suppress_governance_capabilities_display() on the core
  `additional_capabilities_display` filter. Returns false to hide the raw
  section ONLY when every non-role capability the profile user holds is a
  truthy governance cap. If any foreign additional cap is present it returns
  the incoming value unchanged (never hides other plugins' caps, never
  overrides an already-false), and an explicitly denied governance cap is
  left visible as core's "Denied: ...". Non-role classification uses
  wp_roles()->is_role() to mirror core exactly, so additional custom roles
  are not mistaken for stray capabilities.

- render_governance_capabilities_profile_note() on show_user_profile and
  edit_user_profile. Renders a "Sudo capabilities" block listing each
  directly-granted capability by its get_cap_label() label. Reads the raw
  stored grants rather than an effective check, so a multisite super admin
  (effective access via the wp_sudo_can() short-circuit, no stored grant) is
  not misreported. The Access-tab link is shown only to a viewer who can
  reach it (wp_sudo_can('manage_wp_sudo')); others see plain text.

Display-only: enforcement is untouched. Adds 12 unit tests covering the
hide/pass-through/denied/custom-role/no-cap and render branches, one
psalm-baseline entry for the added unqualified wp_sudo_can() call, a 4.8.0
CHANGELOG entry, and refreshed counts in docs/current-metrics.md.

Verified the core filter and its `count(caps) > count(roles) && true ===
$display` render condition against WordPress core wp-admin/user-edit.php
(raw.githubusercontent.com/WordPress/WordPress/master/wp-admin/user-edit.php).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TCJoA72EDAmLviMKDB5zx
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

🎮 Try in WordPress Playground

Open this PR in Playground →

Preview is pinned to WordPress 7.0.

Uses the checked-in blueprint.json from this PR, with the plugin install pinned to commit 4dce3046f78b2f5e555fb8ca34a6cd624056a927 via a GitHub archive .zip (resource: url, fetched through the WordPress Playground CORS proxy).

Logs in as admin / password; use password again for the WP Sudo reauthentication challenge.

✅ What you can test

Feature Notes
Plugin activation & settings page
Gate fires on dangerous actions (plugin activate/delete, user delete, etc.)
Challenge / reauthentication page
Password verification & session cookie
Admin bar countdown timer
Request stash & replay after auth
Rate limiting / 5-attempt lockout ✅ within session
Session expiry by time ✅ wait out the configured duration (1–15 min)
Two Factor plugin (TOTP) ✅ installed automatically via blueprint
unfiltered_html removed from Editor role

❌ What won't work

Feature Why
WP-CLI / Cron entry point policies No CLI in browser
REST / XML-RPC entry point policies Network disabled in Playground
Two Factor email / magic-link providers PHP outbound network is off
Multisite behaviour Single-site only
State after refreshing Playground Full reset on page reload

Transients and user meta persist across normal WP navigation within
a session, but are wiped if you reload the Playground page itself.
Use the integration test suite to verify
transient TTL, real bcrypt, and multisite isolation.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.14%. Comparing base (389d899) to head (4dce304).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #205      +/-   ##
============================================
+ Coverage     90.07%   90.14%   +0.07%     
- Complexity     1540     1554      +14     
============================================
  Files            20       20              
  Lines          5856     5898      +42     
============================================
+ Hits           5275     5317      +42     
  Misses          581      581              
Flag Coverage Δ
unit 90.14% <100.00%> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
includes/class-admin.php 88.82% <100.00%> (+0.34%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

… note

render_governance_capabilities_profile_note() builds the Sudo Access link
through network_admin_url() on multisite and admin_url() on single-site. The
existing render tests all run single-site (TestCase defaults is_multisite()
to false), leaving the multisite branch uncovered (flagged by Codecov on
#205). Add a test that flips is_multisite() true and asserts the link
resolves through /wp-admin/network/ with tab=access.

Refresh the unit-test/line counts in docs/current-metrics.md accordingly
(1,041 tests / 3,131 assertions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TCJoA72EDAmLviMKDB5zx
@dknauss
dknauss marked this pull request as ready for review July 23, 2026 16:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c22acb33aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread includes/class-admin.php
Comment thread docs/current-metrics.md
Comment thread includes/class-admin.php
@dknauss
dknauss enabled auto-merge (squash) July 23, 2026 16:54
@dknauss
dknauss merged commit 7c422fb into main Jul 23, 2026
40 checks passed
@dknauss
dknauss deleted the claude/whats-next-dc6xeq branch July 23, 2026 17:11
dknauss added a commit that referenced this pull request Jul 24, 2026
)

* docs: reconcile release-status + release-environment-log to v4.8.0

Both canonical release docs were stale. release-status.md still named
4.7.0 as latest with "nothing unreleased past it"; release-environment-log.md
stopped at 4.6.0 and its status table contradicted its own 4.6.0 section
("gate pending" vs "cleared").

release-status.md:
- Latest tagged release -> v4.8.0 (cut 2026-07-23, on 10587a4); previous -> v4.7.0
- Add 4.8.0 payload note (REST-gate security hardening #213/#214/#215 +
  role/capability lockdown audit MVP #206 + readable governance caps #205)
- Add v4.8.0 tag checklist; flag the env-matrix decision as a retroactive gap
- Current main state: record unreleased-past-tag work (alert-bridge role-drift
  push #226 + 4.8.0 security-test docs); correct "nothing unreleased" claim
- Update post-tag drift source to v4.8.0..main (no longer empty)

release-environment-log.md:
- Fix stale 4.6.0 status-table row (tagged + cleared, not "gate pending")
- Add v4.7.0 row/section (matrix reused from 4.6.0 by conscious decision,
  per release-status.md v4.7.0 checklist item 2)
- Add v4.8.0 row/section: manual Apache/managed-host/min-WP matrix decision
  not recorded at tag time; 4.8.0 changed server-facing REST routing/method
  matching, so reuse is not obviously justified. Live functional security pass
  exists (security-test-results-4.8.0.md, Studio/SQLite+Redis, headless) but is
  not the environment matrix. Flagged for maintainer reuse-or-rerun decision.

Verified against: git tag v4.8.0 (annotated, 2026-07-23), wp-sudo.php /
readme.txt version constants on main, CHANGELOG 4.8.0 section,
git log v4.7.0..v4.8.0 and v4.8.0..main, docs/security-test-results-4.8.0.md.
Docs-only; both files are in the docs-lint date/count allowlist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: address Codex review — fix email-rule description, WP 7.0.2, and 4 contradictions

Incorporates the automated Codex review findings on #231:

1. (correctness) 4.8.0 payload note conflated two REST fixes. Corrected:
   user.change_password/user.promote were PRE-EXISTING rules matching only
   PUT/PATCH (POST ungated) → now gate POST (#213); user.change_email is a NEW
   rule gating POST/PUT/PATCH (#214). Verified against the CHANGELOG 4.8.0 section.
2. (accuracy) Latest stable WordPress patch was stale ("7.0 GA, June 14").
   Updated to 7.0.2, verified via api.wordpress.org/core/version-check on
   2026-07-24 (matches the WP 7.0.2 env in security-test-results-4.8.0.md).
   Clarified that readme.txt "Tested up to 7.0" tracks the branch, not the patch.
3. (contradiction) v4.8.0 checklist intro claimed "all items done" while item 2
   is the open env-matrix gap — reworded to except item 2.
4. (contradiction) historical 4.5.0 section still said main was "at v4.7.0 with
   nothing unreleased" — updated to 4.8.0 + unreleased post-tag work.
5. (contradiction) env-log 4.6.0 section opened "no v4.6.0 tag cut yet" after its
   row was marked tagged — reframed as the pre-tag gate-decision record.
6. (consistency) nginx-multisite smoke lane: release-status said "de-scoped" but
   ground truth (release-confidence.yml + env-log) shows it was re-hardened and
   restored to the gate post-4.6.0 (2026-07-06). release-status now matches.

Docs-only; both files remain in the docs-lint date allowlist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants