diff --git a/inc/Abilities/AgentAbilities.php b/inc/Abilities/AgentAbilities.php index 4f5a6fd26..928b3122e 100644 --- a/inc/Abilities/AgentAbilities.php +++ b/inc/Abilities/AgentAbilities.php @@ -401,14 +401,21 @@ public static function renameAgent( array $input ): array { } /** - * List all registered agents. + * List registered agents, scoped to the current site. + * + * On multisite, returns agents with site_scope matching the current blog_id + * OR site_scope IS NULL (network-wide). This mirrors WordPress core's default + * of scoping user queries to the current site via wp_N_capabilities meta. + * + * @since 0.38.0 + * @since 0.57.0 Added site_scope filtering and site_scope in output. * * @param array $input Input parameters (unused). * @return array Result. */ public static function listAgents( array $input ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Required by WP_Ability interface. $agents_repo = new Agents(); - $rows = $agents_repo->get_all(); + $rows = $agents_repo->get_all( array( 'site_id' => get_current_blog_id() ) ); $agents = array(); @@ -418,6 +425,7 @@ public static function listAgents( array $input ): array { // phpcs:ignore Gener 'agent_slug' => (string) $row['agent_slug'], 'agent_name' => (string) $row['agent_name'], 'owner_id' => (int) $row['owner_id'], + 'site_scope' => isset( $row['site_scope'] ) ? (int) $row['site_scope'] : null, 'status' => (string) $row['status'], ); } diff --git a/inc/Abilities/PermissionHelper.php b/inc/Abilities/PermissionHelper.php index 585162401..cd0cfe2dd 100644 --- a/inc/Abilities/PermissionHelper.php +++ b/inc/Abilities/PermissionHelper.php @@ -367,9 +367,10 @@ public static function owns_resource( int $resource_user_id, string $action = 'm * Determines which agent's data should be returned based on the request: * - If `agent_id` param is present and caller has access → use that agent_id * - If caller is admin and no `agent_id` param → return null (all agents) - * - If caller is non-admin → resolve their accessible agent IDs + * - If caller is non-admin → resolve via ownership, then access grants * * @since 0.41.0 + * @since 0.57.0 Non-admin fallback checks access grants when user owns no agent. * * @param \WP_REST_Request $request REST request. * @param string $action Action key for admin check (default: 'manage_flows'). @@ -402,7 +403,7 @@ public static function resolve_scoped_agent_id( \WP_REST_Request $request, strin return null; } - // Non-admin with no explicit agent_id: resolve via owner_id lookup. + // Non-admin with no explicit agent_id: resolve via owner_id first. $user_id = self::acting_user_id(); $agents_repo = new \DataMachine\Core\Database\Agents\Agents(); $agent = $agents_repo->get_by_owner_id( $user_id ); @@ -411,6 +412,14 @@ public static function resolve_scoped_agent_id( \WP_REST_Request $request, strin return (int) $agent['agent_id']; } + // Fallback: check access grants (user may have access to an agent they don't own). + $access_repo = new \DataMachine\Core\Database\Agents\AgentAccess(); + $accessible_ids = $access_repo->get_agent_ids_for_user( $user_id ); + + if ( ! empty( $accessible_ids ) ) { + return $accessible_ids[0]; + } + // No agent found — return 0 which will match nothing (safe fallback). return 0; } diff --git a/inc/Api/Agents.php b/inc/Api/Agents.php index 5aad77493..a44fdabb8 100644 --- a/inc/Api/Agents.php +++ b/inc/Api/Agents.php @@ -298,17 +298,25 @@ public static function register_routes(): void { /** * Handle GET /agents — list agents the current user can access. * - * Admins see all agents. Other users see only agents they have access to. + * Mirrors WordPress core's multisite user scoping: queries are scoped to the + * current site by default. Agents with site_scope matching the current blog_id + * OR site_scope IS NULL (network-wide) are returned. Admins see all matching + * agents; non-admins see only agents they have explicit access grants for. + * + * @since 0.41.0 + * @since 0.57.0 Added site_scope filtering based on current blog_id. * * @param WP_REST_Request $request REST request. * @return \WP_REST_Response|WP_Error */ public static function handle_list( WP_REST_Request $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - $agents_repo = new AgentsRepository(); - $user_id = get_current_user_id(); + $agents_repo = new AgentsRepository(); + $user_id = get_current_user_id(); + $current_site = get_current_blog_id(); if ( PermissionHelper::can( 'manage_agents' ) ) { - $all_agents = $agents_repo->get_all(); + // Admins see agents scoped to the current site + network-wide agents. + $all_agents = $agents_repo->get_all( array( 'site_id' => $current_site ) ); } else { $access_repo = new AgentAccess(); $accessible_ids = $access_repo->get_agent_ids_for_user( $user_id ); @@ -322,10 +330,16 @@ public static function handle_list( WP_REST_Request $request ) { // phpcs:ignore ); } + // Filter accessible agents by site scope (current site + network-wide). $all_agents = array(); foreach ( $accessible_ids as $agent_id ) { $agent = $agents_repo->get_agent( $agent_id ); - if ( $agent ) { + if ( ! $agent ) { + continue; + } + + $scope = $agent['site_scope'] ?? null; + if ( null === $scope || (int) $scope === $current_site ) { $all_agents[] = $agent; } } @@ -716,6 +730,9 @@ public static function handle_revoke_token( WP_REST_Request $request ) { /** * Shape an agent row for list output (excludes config which may contain secrets). * + * @since 0.41.0 + * @since 0.57.0 Added site_scope to output. + * * @param array $agent Agent database row. * @return array Shaped output. */ @@ -725,6 +742,7 @@ private static function shape_list_item( array $agent ): array { 'agent_slug' => (string) $agent['agent_slug'], 'agent_name' => (string) $agent['agent_name'], 'owner_id' => (int) $agent['owner_id'], + 'site_scope' => isset( $agent['site_scope'] ) ? (int) $agent['site_scope'] : null, 'status' => (string) $agent['status'], 'created_at' => $agent['created_at'] ?? '', 'updated_at' => $agent['updated_at'] ?? '', diff --git a/inc/Core/Database/Agents/Agents.php b/inc/Core/Database/Agents/Agents.php index 8b91c4384..48bf1959f 100644 --- a/inc/Core/Database/Agents/Agents.php +++ b/inc/Core/Database/Agents/Agents.php @@ -248,18 +248,48 @@ public function update_agent( int $agent_id, array $data ): bool { } /** - * Get all agents. + * Get all agents, optionally filtered by site scope. + * + * Mirrors WordPress core's multisite user scoping pattern: + * - Default (no args): returns ALL agents (network-wide view) + * - With site_id: returns agents scoped to that site OR network-wide (site_scope IS NULL) * * @since 0.38.0 + * @since 0.57.0 Added $args parameter with site_id filtering. + * + * @param array $args { + * Optional. Query arguments. + * + * @type int|null $site_id Blog ID to filter by. Agents with this site_scope + * OR site_scope IS NULL (network-wide) are returned. + * Default null (no filtering — all agents). + * } * @return array List of agent rows. */ - public function get_all(): array { - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared - $rows = $this->wpdb->get_results( - $this->wpdb->prepare( 'SELECT * FROM %i ORDER BY agent_id ASC', $this->table_name ), - ARRAY_A - ); - // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + public function get_all( array $args = array() ): array { + $site_id = $args['site_id'] ?? null; + + if ( null !== $site_id ) { + $site_id = (int) $site_id; + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + $rows = $this->wpdb->get_results( + $this->wpdb->prepare( + 'SELECT * FROM %i WHERE site_scope = %d OR site_scope IS NULL ORDER BY agent_id ASC', + $this->table_name, + $site_id + ), + ARRAY_A + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + } else { + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + $rows = $this->wpdb->get_results( + $this->wpdb->prepare( 'SELECT * FROM %i ORDER BY agent_id ASC', $this->table_name ), + ARRAY_A + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + } if ( ! $rows ) { return array();