Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
613527a
feat: add access control settings for features with REST API and UI i…
Infinite-Null Jun 17, 2026
1ca4935
refactor: implement local state for role and user tokens in AccessCon…
Infinite-Null Jun 18, 2026
013f157
refactor: replace Stack with Flex layout in AccessControlSettings and…
Infinite-Null Jun 18, 2026
c1f6160
refactor: replace FormTokenField with CheckboxControl for role select…
Infinite-Null Jun 18, 2026
c134007
feat: implement debounced server-side user search and persistent sele…
Infinite-Null Jun 18, 2026
a0da3db
Merge branch 'develop' into feat/add-role-user-access-controls
Infinite-Null Jun 18, 2026
1e6c0ae
feat: update access control settings input to use 40px default sizing…
Infinite-Null Jun 18, 2026
4832b12
feat: enforce access control checks for experiment registration and u…
Infinite-Null Jun 19, 2026
bd2ba2a
feat: restrict access control settings to non-admin features
Infinite-Null Jun 19, 2026
615d44b
refactor: add type hinting to REST controller and simplify role verif…
Infinite-Null Jun 19, 2026
25ba583
refactor: format AccessControlSettings conditional rendering for bett…
Infinite-Null Jun 19, 2026
7f28d57
refactor: improve Yoda condition readability in user search query logic
Infinite-Null Jun 19, 2026
3f11239
refactor: remove redundant user login check from access verification …
Infinite-Null Jun 19, 2026
d6ad4d7
refactor: clean up AccessControlSettings hook usage and implement log…
Infinite-Null Jun 22, 2026
c37c5bb
style: refactor code formatting and indentation in access control com…
Infinite-Null Jun 22, 2026
e26725c
fix: sync selected user display names and filter current selections f…
Infinite-Null Jun 22, 2026
993bb99
refactor: reformat loop block in AccessControlSettings and update loc…
Infinite-Null Jun 22, 2026
f1e5478
Merge branch 'develop' into feat/add-role-user-access-controls
Infinite-Null Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ protected function load_metadata(): array {
* {@inheritDoc}
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_assets' ) );
add_action( 'wp_enqueue_media', array( $this, 'enqueue_media_frame_assets' ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ protected function load_metadata(): array {
* {@inheritDoc}
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
Expand Down
4 changes: 4 additions & 0 deletions includes/Experiments/Content_Resizing/Content_Resizing.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ protected function load_metadata(): array {
* {@inheritDoc}
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
Expand Down
4 changes: 4 additions & 0 deletions includes/Experiments/Editorial_Notes/Editorial_Notes.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ protected function load_metadata(): array {
* {@inheritDoc}
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ) );
add_filter( 'rest_pre_insert_comment', array( $this, 'maybe_set_ai_author' ), 10, 2 );
Expand Down
4 changes: 4 additions & 0 deletions includes/Experiments/Editorial_Updates/Editorial_Updates.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ protected function load_metadata(): array {
* @since 0.8.0
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ) );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ protected function load_metadata(): array {
* {@inheritDoc}
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
Expand Down
4 changes: 4 additions & 0 deletions includes/Experiments/Meta_Description/Meta_Description.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ protected function load_metadata(): array {
* @since 0.7.0
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'deactivated_plugin', array( $this, 'clear_active_plugin_cache' ) );
Expand Down
4 changes: 4 additions & 0 deletions includes/Experiments/Summarization/Summarization.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ protected function load_metadata(): array {
* {@inheritDoc}
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

$this->register_post_meta();
add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ), 5 );
Expand Down
4 changes: 4 additions & 0 deletions includes/Experiments/Title_Generation/Title_Generation.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ protected function load_metadata(): array {
* @since 0.1.0
*/
public function register(): void {
if ( ! \WordPress\AI\ai_current_user_can_access_feature( $this->get_id() ) ) {
return;
}

add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
Expand Down
94 changes: 94 additions & 0 deletions includes/REST/Roles_Users_Controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* REST controller for roles and users.
*
* @package WordPress\AI\REST
*/

declare( strict_types=1 );

namespace WordPress\AI\REST;

defined( 'ABSPATH' ) || exit;

class Roles_Users_Controller {

private const API_NAMESPACE = 'ai/v1';

private const ROUTE = '/roles-users';

private const MAX_USERS = 10;

public function init(): void {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}

public function register_routes(): void {
register_rest_route(
self::API_NAMESPACE,
self::ROUTE,
array(
'methods' => 'GET',
'callback' => array( $this, 'get_roles_users' ),
'permission_callback' => array( $this, 'check_permission' ),
'args' => array(
'search' => array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
}

public function check_permission(): bool {
return current_user_can( 'manage_options' );
}

/**
* Returns roles and users for the access control endpoint.
*
* @param \WP_REST_Request $request The REST request.
* @return \WP_REST_Response
*/
public function get_roles_users( \WP_REST_Request $request ): \WP_REST_Response {
$roles = array();

foreach ( wp_roles()->roles as $role_id => $role ) {
$roles[] = array(
'id' => $role_id,
'name' => translate_user_role( $role['name'] ),
);
}

$search = (string) $request->get_param( 'search' );
$get_users_args = array(
'fields' => array( 'ID', 'display_name' ),
'number' => self::MAX_USERS,
);

if ( '' !== $search ) {
$get_users_args['search'] = '*' . $search . '*';
$get_users_args['search_columns'] = array( 'user_login', 'display_name', 'user_email' );
}

$users = array();
$wp_users = get_users( $get_users_args );

foreach ( $wp_users as $user ) {
$users[] = array(
'id' => (int) $user->ID,
'name' => $user->display_name,
);
}

return new \WP_REST_Response(
array(
'roles' => $roles,
'users' => $users,
),
200
);
}
}
36 changes: 36 additions & 0 deletions includes/Settings/Settings_Registration.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use WordPress\AI\Features\Registry;
use WordPress\AI\REST\Models_Controller;
use WordPress\AI\REST\Roles_Users_Controller;

/**
* Handles registration of settings for the AI plugin.
Expand Down Expand Up @@ -71,6 +72,7 @@ public function init(): void {

// Initialize the provider/model discovery REST endpoint.
( new Models_Controller() )->init();
( new Roles_Users_Controller() )->init();
}

/**
Expand Down Expand Up @@ -132,6 +134,40 @@ public function register_settings(): void {
)
);

register_setting(
self::OPTION_GROUP,
"wpai_feature_{$feature_id}_roles",
array(
'type' => 'array',
'default' => array(),
'show_in_rest' => array(
'schema' => array(
'type' => 'array',
'items' => array(
'type' => 'string',
),
),
),
)
);

register_setting(
self::OPTION_GROUP,
"wpai_feature_{$feature_id}_users",
array(
'type' => 'array',
'default' => array(),
'show_in_rest' => array(
'schema' => array(
'type' => 'array',
'items' => array(
'type' => 'integer',
),
),
),
)
);

// Allow experiments to register their own custom settings.
if ( ! method_exists( $feature, 'register_settings' ) ) {
continue;
Expand Down
28 changes: 28 additions & 0 deletions includes/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -692,3 +692,31 @@ function get_min_content_length( string $feature_id, int $content_length = 100 )
*/
return (int) apply_filters( 'wpai_min_content_length', $content_length, $feature_id );
}

/**
* Checks whether the current user has access to a given feature based on access control settings.
*
* 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
*
* @param string $feature_id The ID of the feature/experiment.
* @return bool True if the user has access, false otherwise.
*/
function ai_current_user_can_access_feature( string $feature_id ): bool {
$roles = get_option( "wpai_feature_{$feature_id}_roles", array() );
$users = get_option( "wpai_feature_{$feature_id}_users", array() );

if ( empty( $roles ) && empty( $users ) ) {
return true;
}

$current_user = wp_get_current_user();

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

return (bool) array_intersect( $current_user->roles, $roles );
}
Loading
Loading