Skip to content

Feat/add role user access controls - #749

Open
Infinite-Null wants to merge 26 commits into
WordPress:developfrom
Infinite-Null:feat/add-role-user-access-controls
Open

Feat/add role user access controls#749
Infinite-Null wants to merge 26 commits into
WordPress:developfrom
Infinite-Null:feat/add-role-user-access-controls

Conversation

@Infinite-Null

@Infinite-Null Infinite-Null commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Closes: #736

Description

This PR introduces Feature-Level Access Controls exclusively for Editor Experiments, allowing site administrators to restrict access to individual AI-powered editor features based on specific WordPress roles or individual users.

Testing Instructions

  1. Navigate to AI Settings and enable Enable AI.
  2. Open the Developer Tools menu (three dots) and select Access controls.
  3. Under the "Editor Experiments" section, verify that the Roles and Users configuration panel appears beneath the feature toggles.
  4. Check a few roles (e.g., Editor, Author) for a specific feature and click Save. Verify the save is successful.
  5. In the Users field, search for a specific user and select them from the dropdown. Click Save.
  6. Refresh the page and verify that your selected roles and users persisted and display their correct names.
  7. Turn off "Access controls" from the Developer Tools menu, and verify that all selections are cleared.
  8. Verify Enforcements: Log in to the site as a user who does not possess the selected role and is not the selected user. Open the block editor and verify that the restricted experiment is not accessible or visible to them.
  9. Log in as the specifically selected user or a user with the granted role, and verify the feature is fully accessible.
  10. Turn off "Access controls" from the Developer Tools menu, and verify that all selections are cleared and the feature becomes globally available again.

Screencast

User: Ankit Shah | Role: Editor

Screen.Recording.2026-06-22.at.1.35.17.PM-compressed.mp4

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Sonnet 4.6
Used for: Validating bug, suggesting a fix.

Changelog Entry

Add - Role and User based access controls for experiments

Open WordPress Playground Preview

@Infinite-Null
Infinite-Null marked this pull request as draft June 19, 2026 13:05
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

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.

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

Co-authored-by: Infinite-Null <ankitkumarshah@git.wordpress.org>
Co-authored-by: dkotter <dkotter@git.wordpress.org>
Co-authored-by: jeffpaul <jeffpaul@git.wordpress.org>
Co-authored-by: henryperkins <htperkins@git.wordpress.org>

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

@Infinite-Null
Infinite-Null marked this pull request as ready for review June 22, 2026 08:12
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.09901% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.53%. Comparing base (8c3606a) to head (2c1f94d).
⚠️ Report is 5 commits behind head on develop.

Files with missing lines Patch % Lines
...iments/Alt_Text_Generation/Alt_Text_Generation.php 50.00% 1 Missing ⚠️
.../Content_Classification/Content_Classification.php 50.00% 1 Missing ⚠️
.../Experiments/Content_Resizing/Content_Resizing.php 50.00% 1 Missing ⚠️
...es/Experiments/Editorial_Notes/Editorial_Notes.php 50.00% 1 Missing ⚠️
...xperiments/Editorial_Updates/Editorial_Updates.php 50.00% 1 Missing ⚠️
...eriments/Excerpt_Generation/Excerpt_Generation.php 50.00% 1 Missing ⚠️
.../Experiments/Meta_Description/Meta_Description.php 50.00% 1 Missing ⚠️
...cludes/Experiments/Summarization/Summarization.php 50.00% 1 Missing ⚠️
.../Experiments/Title_Generation/Title_Generation.php 50.00% 1 Missing ⚠️
includes/helpers.php 85.71% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #749      +/-   ##
=============================================
+ Coverage      78.40%   78.53%   +0.13%     
- Complexity      2454     2470      +16     
=============================================
  Files            104      105       +1     
  Lines           9918    10027     +109     
=============================================
+ Hits            7776     7875      +99     
- Misses          2142     2152      +10     
Flag Coverage Δ
unit 78.53% <90.09%> (+0.13%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 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.

@jeffpaul

Copy link
Copy Markdown
Member

@Infinite-Null FYI some merge conflicts to resolve to keep this moving along

@jeffpaul jeffpaul mentioned this pull request Jul 13, 2026
28 tasks
@Infinite-Null
Infinite-Null requested a review from a team as a code owner July 14, 2026 13:11
Added - New "Advanced settings" option in Developer Tools to show/hide additional configuration options for features and experiments.
Changed - Move all existing configuration options into a new "Advanced settings" section which is hidden by default.

Co-authored-by: Infinite-Null <ankitkumarshah@git.wordpress.org>
Co-authored-by: jeffpaul <jeffpaul@git.wordpress.org>
Co-authored-by: dkotter <dkotter@git.wordpress.org>
@Infinite-Null

Copy link
Copy Markdown
Contributor Author

Thanks for the heads up, @jeffpaul! I've resolved the merge conflicts now. Whenever you have a chance, could you please take another look? Thanks!

@dkotter dkotter modified the milestones: 1.2.0, 1.3.0 Jul 14, 2026
@henryperkins

Copy link
Copy Markdown
Contributor

Thanks for working on this. I maintain Flavor Agent, a third-party Abstract_Feature that uses the AI plugin’s feature framework while registering its editor assets, recommendation/apply abilities, and dedicated MCP server through separate bootstrap paths.

The current implementation appears to enforce access by calling ai_current_user_can_access_feature() inside each built-in Editor Experiment’s register() method. That handles those experiments, but I do not think it yet defines a complete contract for third-party features.

In Flavor Agent, for example, Abstract_Feature::register() owns the editor asset hook, while the feature’s Abilities and MCP server are registered elsewhere. An unauthorized user could therefore lose the editor UI while related programmatic surfaces remain registered unless the plugin independently knows which helper and option contract to call.

Would it make sense to:

  1. Apply the access check centrally in the Feature Loader before invoking register().
  2. Treat ai_current_user_can_access_feature( $feature_id ) as a documented, stable third-party API.
  3. Add a test-only third-party Abstract_Feature fixture.
  4. Document that separate Ability, REST, CLI, or MCP execution paths must use the same resolver in their permission boundary.
  5. Include a test proving that an unauthorized direct Ability execution is denied rather than merely hidden from the settings/editor UI.

I can contribute the failing third-party integration test and the centralized enforcement change. The important acceptance criterion for me is that an unauthorized user receives neither the first-party feature surface nor an executable programmatic path, while existing WordPress capability checks remain in force.

* {@inheritDoc}
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't love forcing each experiment to run this same code. Since all experiments extend Abstract_Feature, could a check like this live there so any experiment that extends that class automatically gets this behavior? Or maybe even better in our Loader class?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For example, this PR doesn't apply this to Image Generation or Type Ahead

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The other issue to consider is that some features do other things in this register method beyond just loading the UI and the Ability. For instance, custom post meta is registered or caches are cleared. Those things will no longer run for certain users, if restriction is turned on, leading to weird edge cases that will be hard to debug. Ideally we figure out a way to prevent the actual experiment from running but still allow other pieces to function properly

/**
* Roles_Users_Controller test case.
*
* @since 0.9.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All @since statements in this PR need to be set to x.x.x


private const MAX_USERS = 10;

public function init(): void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any reason to not have docblocks on all methods and constants in this class?

);

if ( '' !== $search ) {
$get_users_args['search'] = '*' . $search . '*';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any escaping we should be applying here?

Comment thread includes/helpers.php Outdated
* If no roles or users are explicitly configured for the feature, it allows access by default.
* If there are configured roles/users, the current user must match at least one role or be explicitly listed.
*
* @since 0.1.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @since 0.1.0
* @since x.x.x

Comment thread includes/helpers.php

$current_user = wp_get_current_user();

if ( in_array( $current_user->ID, $users, true ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if ( in_array( $current_user->ID, $users, true ) ) {
if ( in_array( $current_user->ID, array_map( 'intval', (array) $users ), true ) ) {

Comment thread includes/helpers.php
$roles = get_option( "wpai_feature_{$feature_id}_roles", array() );
$users = get_option( "wpai_feature_{$feature_id}_users", array() );

if ( empty( $roles ) && empty( $users ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we also need an is_array check here on these?

)
);

register_setting(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Might want to add a sanitize_callback to both of these to ensure we only save real user roles and real user IDs

</p>
) }
{ ! isLoading && ! fetchError && (
<>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this fragment necessary?

accessibleWhenDisabled
>
{ isSaving ? (
<Spinner />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No need to display a spinner here, we can rely on isBusy

@dkotter dkotter modified the milestones: 1.3.0, Future Release Jul 31, 2026
@jeffpaul

Copy link
Copy Markdown
Member

@Infinite-Null please let us know when you're updated from comments/code review feedback for us to test this out again, thanks!

@jeffpaul jeffpaul modified the milestones: Future Release, 1.4.0 Aug 10, 2026
@Infinite-Null

Copy link
Copy Markdown
Contributor Author

@Infinite-Null please let us know when you're updated from comments/code review feedback for us to test this out again, thanks!

Hey @jeffpaul, I am out till 16th August due to personal commitments. I will update the comments/code review after then. Thank you for understanding.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose role/user access controls per feature/experiment

4 participants