-
Notifications
You must be signed in to change notification settings - Fork 27
Fix has_permission() return type inconsistency (Issue #67) #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix has_permission() return type inconsistency (Issue #67) #76
Conversation
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 Unlinked AccountsThe 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.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
@justlevine, thanks for keeping me aware of what is happening. You can review the fix itself now |
fb27297
to
8fcfcac
Compare
There was a problem hiding this 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.

*/ | ||
public function execute( $input = null ) { | ||
$has_permissions = $this->has_permission( $input ); | ||
$has_permissions = $this->get_permission_status( $input ); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
@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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@justlevine Thanks for the feedback! Based on your comments and WP Core research, I’ve updated the PR to: These changes address #67, improve security, and follow Core conventions for REST and capability checks. |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
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). |
There was a problem hiding this 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.
2f742bd
to
4beab25
Compare
There was a problem hiding this 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 problematichas_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.
@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 |
@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? 🤔 |
The REST API uses |
There was a problem hiding this 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?
I'm sorry to be pedantic, but as I previously noted the REST API uses 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. |
There was a problem hiding this 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 ); |
There was a problem hiding this comment.
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.
if ( $ability ) { | ||
// Check permissions first - always use is_wp_error() to handle errors properly | ||
$permission = $ability->check_permission(); |
There was a problem hiding this comment.
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 );
}
}
|
||
```php | ||
// Method signature: | ||
// check_permission( $input = null ) | ||
``` |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 ) ) { |
There was a problem hiding this comment.
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
if ( ! $ability->check_permission( $input ) ) { | |
if ( true !== $ability->check_permission( $input ) ) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
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. |
A follow-up #94 is ready for review. It addresses all feedback from @justlevine, but #76 (comment), which I think needs more thought. |
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 eitherbool
orWP_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 clearbool|WP_Error
return typehas_permission($input)
: Deprecated wrapper for backward compatibilityis_wp_error()
usage patterns throughout$skip_validation
parameter for REST API permission checksTesting
Existing comprehensive tests verify both methods work correctly:
has_permission()
methodFixes #67