create([ 'username' => $username, 'name' => 'Role Boundary Manager', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $role = AdminRole::query()->create([ 'slug' => 'boundary_'.$username, 'name' => 'Boundary Manager', 'scope_type' => AdminRole::SCOPE_SYSTEM, ]); $role->syncLegacyPermissionSlugs(['prd.admin_user.manage', 'prd.admin_role.manage']); $admin->roles()->sync([ (int) $role->id => [ 'site_id' => AdminUser::requireDefaultAdminSiteId(), 'granted_at' => now(), ], ]); return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; } function createPlatformRoleBoundaryAgent(string $username): AdminUser { $siteId = AdminUser::requireDefaultAdminSiteId(); $root = AgentNode::query() ->where('admin_site_id', $siteId) ->where('depth', 0) ->firstOrFail(); $super = AdminUser::query()->create([ 'username' => 'super_'.$username, 'name' => 'Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $node = app(AgentNodeService::class)->createChild($super, [ 'parent_id' => (int) $root->id, 'code' => 'node-'.$username, 'name' => 'Agent '.$username, 'username' => $username, 'password' => 'secret-strong', ]); $adminUserId = DB::table('admin_user_agents') ->where('agent_node_id', $node->id) ->where('is_primary', true) ->value('admin_user_id'); return AdminUser::query()->findOrFail((int) $adminUserId); } test('permission catalog keeps all roles but only exposes platform assignable roles', function (): void { $token = makePlatformRoleBoundaryToken('catalog_boundary'); $data = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-user-permission-catalog') ->assertOk() ->json('data'); expect(collect($data['roles'])->pluck('slug')->all()) ->toContain(PlatformSystemRoles::SLUG_AGENT, PlatformSystemRoles::SLUG_SUPER_ADMIN); expect(collect($data['assignable_roles'])->pluck('slug')->all()) ->not->toContain(PlatformSystemRoles::SLUG_AGENT, PlatformSystemRoles::SLUG_SUPER_ADMIN); }); test('platform account create and role update reject the agent role', function (): void { $token = makePlatformRoleBoundaryToken('assignment_boundary'); $siteId = AdminUser::requireDefaultAdminSiteId(); $this->withHeader('Authorization', 'Bearer '.$token) ->withHeader('X-Locale', 'zh') ->postJson('/api/v1/admin/admin-users', [ 'username' => 'illegal_agent_platform', 'nickname' => 'Illegal Agent Platform', 'email' => null, 'password' => 'secret-strong', 'status' => 0, 'admin_site_id' => $siteId, 'role_slugs' => [PlatformSystemRoles::SLUG_AGENT], ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value) ->assertJsonPath('msg', '平台账号不能分配“代理”角色;请在“代理管理”中创建或绑定代理账号。'); expect(AdminUser::query()->where('username', 'illegal_agent_platform')->exists())->toBeFalse(); $target = AdminUser::query()->create([ 'username' => 'platform_role_target', 'name' => 'Platform Role Target', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-users/'.$target->id.'/roles', [ 'admin_site_id' => $siteId, 'role_slugs' => [PlatformSystemRoles::SLUG_AGENT], ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); }); test('agent creation keeps its agent role and cleanup removes only unbound platform assignments', function (): void { $agentUser = createPlatformRoleBoundaryAgent('legal_agent_boundary'); $agentRole = AdminRole::query() ->where('scope_type', AdminRole::SCOPE_SYSTEM) ->where('slug', PlatformSystemRoles::SLUG_AGENT) ->firstOrFail(); $siteId = AdminUser::requireDefaultAdminSiteId(); expect(DB::table('admin_user_agents')->where('admin_user_id', $agentUser->id)->exists())->toBeTrue(); expect(DB::table('admin_user_site_roles') ->where('admin_user_id', $agentUser->id) ->where('role_id', $agentRole->id) ->exists())->toBeTrue(); $illegalUser = AdminUser::query()->create([ 'username' => 'unbound_agent_role', 'name' => 'Unbound Agent Role', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); DB::table('admin_user_site_roles')->insert([ 'admin_user_id' => $illegalUser->id, 'site_id' => $siteId, 'role_id' => $agentRole->id, 'granted_at' => now(), ]); $otherSiteId = (int) DB::table('admin_sites')->insertGetId([ 'code' => 'boundary-other-site', 'name' => 'Boundary Other Site', 'is_default' => false, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('admin_user_site_roles')->insert([ 'admin_user_id' => $agentUser->id, 'site_id' => $otherSiteId, 'role_id' => $agentRole->id, 'granted_at' => now(), ]); $ordinaryRole = AdminRole::query()->create([ 'slug' => 'cross_site_ordinary_role', 'name' => 'Cross Site Ordinary Role', 'scope_type' => AdminRole::SCOPE_SYSTEM, ]); DB::table('admin_user_site_roles')->insert([ 'admin_user_id' => $agentUser->id, 'site_id' => $otherSiteId, 'role_id' => $ordinaryRole->id, 'granted_at' => now(), ]); expect(InvalidPlatformAgentRoleCleanup::run())->toBe(2); expect(DB::table('admin_user_site_roles') ->where('admin_user_id', $illegalUser->id) ->where('role_id', $agentRole->id) ->exists())->toBeFalse(); expect(DB::table('admin_user_site_roles') ->where('admin_user_id', $agentUser->id) ->where('site_id', $siteId) ->where('role_id', $agentRole->id) ->exists())->toBeTrue(); expect(DB::table('admin_user_site_roles') ->where('admin_user_id', $agentUser->id) ->where('site_id', $otherSiteId) ->where('role_id', $agentRole->id) ->exists())->toBeFalse(); expect(DB::table('admin_user_site_roles') ->where('admin_user_id', $agentUser->id) ->where('site_id', $otherSiteId) ->where('role_id', $ordinaryRole->id) ->exists())->toBeTrue(); expect(AdminUser::query()->whereKey($illegalUser->id)->exists())->toBeTrue(); }); test('role counts classify platform and agent accounts and role filter never leaks agents', function (): void { $token = makePlatformRoleBoundaryToken('count_boundary'); $siteId = AdminUser::requireDefaultAdminSiteId(); $role = AdminRole::query()->create([ 'slug' => 'counted_role', 'name' => 'Counted Role', 'scope_type' => AdminRole::SCOPE_SYSTEM, ]); $platformUser = AdminUser::query()->create([ 'username' => 'counted_platform', 'name' => 'Counted Platform', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $platformUser->roles()->sync([ (int) $role->id => [ 'site_id' => $siteId, 'granted_at' => now(), ], ]); $agentUser = createPlatformRoleBoundaryAgent('counted_agent'); DB::table('admin_user_site_roles')->insert([ 'admin_user_id' => $agentUser->id, 'site_id' => $siteId, 'role_id' => $role->id, 'granted_at' => now(), ]); $roleRow = collect($this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-roles') ->assertOk() ->json('data.items')) ->firstWhere('slug', 'counted_role'); expect($roleRow) ->not->toBeNull() ->and($roleRow['platform_user_count'])->toBe(1) ->and($roleRow['agent_user_count'])->toBe(1) ->and($roleRow['user_count'])->toBe(2); $items = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-users?role_slug=counted_role') ->assertOk() ->json('data.items'); expect(collect($items)->pluck('username')->all()) ->toBe(['counted_platform']) ->not->toContain('counted_agent'); });