create(); $tenant = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), ]); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'manager', ]); ManagedEnvironmentMembership::query()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'readonly', 'source' => 'manual', ]); $resolver = app(CapabilityResolver::class); expect($resolver->getRole($user, $tenant)?->value)->toBe('manager') ->and($resolver->can($user, $tenant, Capabilities::TENANT_MANAGE))->toBeTrue() ->and($resolver->can($user, $tenant, Capabilities::TENANT_DELETE))->toBeFalse(); }); it('does not fall back to a role-bearing managed-environment membership without workspace membership', function (): void { $workspace = Workspace::factory()->create(); $tenant = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), ]); $user = User::factory()->create(); ManagedEnvironmentMembership::query()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', 'source' => 'manual', ]); $resolver = app(CapabilityResolver::class); expect($resolver->isMember($user, $tenant))->toBeFalse() ->and($resolver->getRole($user, $tenant))->toBeNull() ->and($resolver->can($user, $tenant, Capabilities::TENANT_MANAGE))->toBeFalse(); }); it('logs boundary-safe denied access diagnostics', function (): void { Log::spy(); $workspace = Workspace::factory()->create(); $tenant = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), ]); $user = User::factory()->create(); expect(app(CapabilityResolver::class)->can($user, $tenant, Capabilities::PROVIDER_VIEW))->toBeFalse(); Log::shouldHaveReceived('warning') ->with('rbac.denied', Mockery::on(fn (array $context): bool => $context['failed_boundary'] === 'workspace_membership' && $context['workspace_id'] === (int) $workspace->getKey() && $context['managed_environment_id'] === (int) $tenant->getKey() && $context['actor_user_id'] === (int) $user->getKey() && $context['capability'] === Capabilities::PROVIDER_VIEW)) ->once(); });