Skip to content

Conversation

Ref34t
Copy link
Contributor

@Ref34t Ref34t commented Sep 14, 2025

Summary

Simplifies permission checking API and updates documentation to show proper is_wp_error() usage patterns for preventing WP_Error truthiness issues.

Problem Solved

The original has_permission() method could return either bool or WP_Error, creating a dangerous bug where WP_Error objects (which are truthy) could incorrectly pass permission checks in if statements.

Implementation

  • check_permissions($input, $skip_validation = false): New recommended method with clear bool|WP_Error return type
  • has_permission($input): Deprecated wrapper for backward compatibility
  • Updated documentation: Shows proper is_wp_error() usage patterns throughout
  • Streamlined API: Uses $skip_validation parameter for REST API permission checks

Testing

Existing comprehensive tests verify both methods work correctly:

  • Permission validation with proper error handling
  • Input validation integration with permission checks
  • Backward compatibility for deprecated has_permission() method

Fixes #67

Copy link

github-actions bot commented Sep 14, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @[email protected].

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Unlinked contributors: [email protected].

Co-authored-by: Ref34t <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: justlevine <[email protected]>
Co-authored-by: johnbillion <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@justlevine
Copy link
Contributor

(@Ref34t keep #68 in mind (and #73) before you waste too much time because of failing tests - I'll give a real review one you tag ppl, just saw the rest errors and guessing there might be an unnecessary headache you dont need to care about)

@Ref34t
Copy link
Contributor Author

Ref34t commented Sep 14, 2025

@justlevine, thanks for keeping me aware of what is happening. You can review the fix itself now

@Ref34t Ref34t force-pushed the feature/fix-has-permission-return-type-issue-67 branch from fb27297 to 8fcfcac Compare September 14, 2025 10:38
Copy link
Contributor

@justlevine justlevine left a comment

Choose a reason for hiding this comment

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

Aside from whether or not we need both a public checker function and a (also public) wrapper whose sole purpose is to coerce it to a bool, I think we need a better name for the function itself.

image

*/
public function execute( $input = null ) {
$has_permissions = $this->has_permission( $input );
$has_permissions = $this->get_permission_status( $input );
Copy link
Contributor

Choose a reason for hiding this comment

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

Case in point to #76 (comment) - we're only using this internally while our only use of ::has_permission() is in a REST API endpoint (which long term should probably be an ability anyway

Copy link
Contributor

Choose a reason for hiding this comment

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

while our only use of ::has_permission() is in a REST API endpoint

Follow up: 👆(and not #73) is the reason why the tests are failing. WP_REST_Abilities_Run_Controller::run_ability_permissions_check() explicitly only checks against false and specifically allows the WP_Error through so invalid method errors are only shown to a user with the correct permissions when the callback is actually run:

https://github.com/ref34t/abilities-api/blob/8fcfcac9136dee73abb798bbaded0a21c17b5cd8/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php#L93-L111

@Ref34t In the context of this PR, change ::run_ability_permissions_check() to if ( false === $ability->get_permission_status() (or whatever it's renamed to).


More broadly, I'm like 95% sure this leaked in from the A8C prior art and should be removed and punted to MCP Adapter, as "Resource" and "Tool" are not concepts of the Abilities API and don't belong in core. @emdashcodes / @gziolo please confirm, but either way it can be handled as part of #75.

Copy link
Member

Choose a reason for hiding this comment

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

More broadly, I'm like 95% sure this leaked in from the A8C prior art and should be removed and punted to MCP Adapter, as "Resource" and "Tool" are not concepts of the Abilities API and don't belong in core. @emdashcodes / @gziolo please confirm, but either way it can be handled as part of #75.

I noticed this when reviewing the PR, and I found it useful. I’m not 100% sure we should use this specific set of criteria to distinguish between GET and POST, but I didn’t have the bandwidth to start a larger conversation about the optimal approach as this seems more like an implementation detail. I think we could tap into Add property for marking Ability hints like destructive, read-only, idempotent to iron out the detection mechanism.

Overall, I’m tempted to borrow some of the solutions from MCP without using nomenclature and structure too tied this particular protocol.

Copy link
Contributor

Choose a reason for hiding this comment

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

Overall, I’m tempted to borrow some of the solutions from MCP without using nomenclature and structure too tied this particular protocol.

Yeah it's that latter part that's critical, and considering that MCP's value as a whole is doubtful, I would want a discussion of what a "resource" or "tool" is meant to be in our context, and whether it is inherently useful - if/when this gets revisited.

Either way, no longer part of this PR, so can be resolved.

@Ref34t
Copy link
Contributor Author

Ref34t commented Sep 14, 2025

@justlevine Thanks for the feedback! Based on your comments and WP Core research, I’ve updated the PR to:
• Rename has_permission() → check_permissions() to avoid the misleading has_* boolean expectation.
• Standardize return types to true | false | WP_Error, matching Core patterns (like permissions_check()) and preventing WP_Error from being treated as true.
• Update execute() to always enforce permissions safely, centralizing logic and ensuring abilities can’t be bypassed.
• Align with Core standards while keeping developer usage simple and predictable.

These changes address #67, improve security, and follow Core conventions for REST and capability checks.

Copy link

codecov bot commented Sep 14, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.69%. Comparing base (a33e2be) to head (027c447).
⚠️ Report is 1 commits behind head on trunk.

Additional details and impacted files
@@             Coverage Diff              @@
##              trunk      #76      +/-   ##
============================================
+ Coverage     85.64%   85.69%   +0.05%     
- Complexity      102      103       +1     
============================================
  Files            16       16              
  Lines           773      776       +3     
  Branches         86       86              
============================================
+ Hits            662      665       +3     
  Misses          111      111              
Flag Coverage Δ
javascript 92.66% <ø> (ø)
unit 82.97% <100.00%> (+0.09%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 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.

@Ref34t Ref34t requested a review from justlevine September 14, 2025 23:56
@gziolo
Copy link
Member

gziolo commented Sep 15, 2025

You are exploring more advanced refactorings rather than a simple documentation update to account for possible incorrect usage. I left some other ideas to consider in #67 (comment) and #67 (comment).

@gziolo gziolo added the [Type] Bug Something isn't working label Sep 15, 2025
Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

I wanted to echo the feedback shared by @justlevine in #76 (comment). Can we limit this PR to renaming has_permission() to check_permission() with the included deprecation logic and documentation changes?

That part is ready to go.

@Ref34t Ref34t closed this Sep 26, 2025
@Ref34t Ref34t force-pushed the feature/fix-has-permission-return-type-issue-67 branch from 2f742bd to 4beab25 Compare September 26, 2025 12:29
@Ref34t Ref34t reopened this Sep 26, 2025
@gziolo gziolo requested a review from Copilot September 26, 2025 12:40
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Fixes a dangerous return type inconsistency in the has_permission() method that could cause WP_Error objects to incorrectly pass permission checks due to their truthiness. The solution introduces a new check_permission() method with clear return semantics and deprecates the old method for backward compatibility.

  • Introduces check_permission() method to replace problematic has_permission() with consistent API
  • Deprecates has_permission() while maintaining backward compatibility through delegation
  • Updates all method calls and documentation to use proper is_wp_error() checking patterns

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
includes/abilities-api/class-wp-ability.php Adds new check_permission() method and deprecates has_permission()
includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php Updates REST API to use new check_permission() method
tests/unit/abilities-api/wpRegisterAbility.php Updates tests to use new method and adds deprecation test
docs/4.using-abilities.md Updates documentation with proper is_wp_error() usage patterns
docs/2.getting-started.md Updates getting started guide with correct error handling

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@gziolo
Copy link
Member

gziolo commented Sep 26, 2025

@Ref34t, I see you are actively working on addressing feedback. I triggered review from GitHub Copilot to see if it catches anything. I'm still not sure whether check_permission or check_permissions would be a better choice. The part that triggered my thinking was documentation which uses plural everywhere.

@Ref34t
Copy link
Contributor Author

Ref34t commented Sep 26, 2025

@gziolo I had it before plural and now due to your last feedback I changed it again 😔 what do you think the final call here? 🤔

@gziolo
Copy link
Member

gziolo commented Sep 26, 2025

The REST API uses permission_callback, so let's stick with the singular form, as it is also used for the ability. It looks like no further changes are necessary because in the docs I see the plural form when explaining the concept for REST API:

https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/#permissions-callback

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

All feedback addressed. It looks good to go from my angle. @justlevine how about you?

@jonathanbossenger, could you perform a quick sanity check on the docs changes?

@justlevine
Copy link
Contributor

The REST API uses permission_callback, so let's stick with the singular form, as it is also used for the ability.

I'm sorry to be pedantic, but as I previously noted the REST API uses check_{*_}_permissions() plural, even though the registry arg is permission_callback. It's also more grammatically accurate when paired with the verb check_*.

I'm happy to do this myself in a follow-up PR if we feel bad about making @Ref34t keep swapping back-and-forth, but I do feel strongly about uniformity with core APIs.

Copy link
Contributor

@justlevine justlevine left a comment

Choose a reason for hiding this comment

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

Looks good! Left some comments that have nothing to do with naming things.

*/
public function execute( $input = null ) {
$has_permissions = $this->has_permission( $input );
$has_permissions = $this->get_permission_status( $input );
Copy link
Contributor

Choose a reason for hiding this comment

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

Overall, I’m tempted to borrow some of the solutions from MCP without using nomenclature and structure too tied this particular protocol.

Yeah it's that latter part that's critical, and considering that MCP's value as a whole is doubtful, I would want a discussion of what a "resource" or "tool" is meant to be in our context, and whether it is inherently useful - if/when this gets revisited.

Either way, no longer part of this PR, so can be resolved.

Comment on lines +136 to +138
if ( $ability ) {
// Check permissions first - always use is_wp_error() to handle errors properly
$permission = $ability->check_permission();
Copy link
Contributor

@justlevine justlevine Sep 27, 2025

Choose a reason for hiding this comment

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

I think this (and the previous example) is incorrect. By calling $ability->execute(), $ability->get_permission() gets called, there's rarely a reason to explicitly check permissions as a separate step.

if ( $ability  ) {
        $site_title = $ability->execute();
        // $site_title now holds the result of get_bloginfo('name')
        // error_log( 'Site Title: ' . $site_title );
    }
}

Comment on lines +130 to +134

```php
// Method signature:
// check_permission( $input = null )
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this intentional or did it leak?


// Check permission before execution
if ( $ability->has_permission( $input ) ) {
// Check permission before execution - always use is_wp_error() first
Copy link
Contributor

Choose a reason for hiding this comment

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

I know the doc in general uses a lot of unnecessary nesting, but I think this also make's it clearer than sandwiching the happy-path between two error states.

    // Use a strict check to catch both false and WP_Error
    $permission = $ability->check_permission( $input );
    if ( true === $permission ) {
        // Permission granted - safe to execute.
        $result = $ability->execute( $input );
        if ( is_wp_error( $result ) ) {
            // Handle execution error
            echo 'Execution error: ' . $result->get_error_message();
        } else {
            // Use $result
            if ( $result['success'] ) {
                echo 'Option updated successfully!';
                echo 'Previous value: ' . $result['previous_value'];
            }
        }
    } else {
        // Don't leak permission errors to unauthenticated users.
        if ( is_wp_error( $permission ) ) {
            error_log( 'Permission check failed: ' . $permission->get_error_message() );
        }

        echo 'You do not have permission to execute this ability.';
    }
}

Though imo we shouldn't be actively promoting check_permission() and instead have users fully handle execute()

$ability = wp_get_ability( 'my/ability' );

$result = $ability->execute( $input ) ;
// If we want to only handle perm errors and not also invalid inputs etc.
if ( is_wp_error() && 'ability_invalid_permissions' === $result->get_error_code() ) {
 // handle the error
}


$input = $this->get_input_from_request( $request );
if ( ! $ability->has_permission( $input ) ) {
if ( ! $ability->check_permission( $input ) ) {
Copy link
Contributor

@justlevine justlevine Sep 27, 2025

Choose a reason for hiding this comment

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

Are we intentionally only handling false and saving the WP_Errors for ->execute()? cc @emdashcodes

Suggested change
if ( ! $ability->check_permission( $input ) ) {
if ( true !== $ability->check_permission( $input ) ) {

Copy link
Member

Choose a reason for hiding this comment

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

Let's fix it. I don't think it was intentional.

Copy link
Member

Choose a reason for hiding this comment

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

In my testing, the proposed code changes fail the result of 4 tests in the run controller. We would have to better handle the reason the request errored so it doesn't default to 403. It probably means, we would have to move some logic to run_ability_permissions_check.

Copy link
Member

Choose a reason for hiding this comment

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

An alternative would be to remove run_ability_permissions_check() (skip permission_callback) in the run controller, taking into account that run_ability (part of the callback) validates permissions anyway. It still would have to handle 403 when check_permissions() from the ability returns false.

Technically speaking, we are good. However, we can improve the implementation/documentation to avoid future misunderstandings.

@gziolo
Copy link
Member

gziolo commented Sep 30, 2025

I'm sorry to be pedantic, but as #76 (comment) the REST API uses check_{_}permissions() plural, even though the registry arg is permission_callback. It's also more grammatically accurate when paired with the verb check.

I'm not a native speaker, so I'm happy to follow your guidance here. I will land this PR and try address all the feedback for docs in another PR.

@gziolo gziolo enabled auto-merge (squash) September 30, 2025 08:00
@gziolo gziolo merged commit cbe8a59 into WordPress:trunk Sep 30, 2025
20 checks passed
@gziolo
Copy link
Member

gziolo commented Sep 30, 2025

A follow-up #94 is ready for review. It addresses all feedback from @justlevine, but #76 (comment), which I think needs more thought.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rename usage of has_permission() to account for a WP_Error

3 participants