TenantAtlas/tests/Feature/Filament/TenantRbacWizardTest.php

614 lines
24 KiB
PHP

<?php
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
use App\Http\Controllers\RbacDelegatedAuthController;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
config()->set('tenantpilot.features.conditional_access', false);
});
function tenantWithApp(): Tenant
{
return Tenant::create([
'tenant_id' => 'tenant-guid',
'name' => 'Tenant One',
'app_client_id' => 'client-123',
'app_client_secret' => 'secret',
'status' => 'active',
]);
}
test('rbac action prompts login when no delegated token', function () {
$tenant = tenantWithApp();
$user = User::factory()->create();
$this->actingAs($user);
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
->mountAction('setup_rbac')
->setActionData([
'role_definition_id' => 'role-1',
'role_display_name' => 'Policy and Profile Manager',
'scope' => 'all_devices',
'group_mode' => 'create',
])
->callMountedAction()
->assertHasNoActionErrors();
expect(Cache::get(RbacDelegatedAuthController::cacheKey($tenant, $user->id, session()->getId())))->toBeNull();
});
test('rbac action succeeds and clears token cache', function () {
$tenant = tenantWithApp();
$user = User::factory()->create();
$this->actingAs($user);
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
app()->bind(GraphClientInterface::class, function () {
return new class implements GraphClientInterface
{
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
$filter = $options['query']['$filter'] ?? '';
if ($method === 'GET' && $path === 'servicePrincipals') {
return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]);
}
if ($method === 'GET' && $path === 'groups' && str_contains($filter, 'displayName eq')) {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'POST' && $path === 'groups') {
return new GraphResponse(true, ['id' => 'group-1']);
}
if ($method === 'POST' && str_contains($path, '/members/$ref')) {
return new GraphResponse(true, []);
}
if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') {
return new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]);
}
if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'POST' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['id' => 'assign-1']);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) {
return new GraphResponse(true, ['value' => []]);
}
return new GraphResponse(true, ['value' => []]);
}
};
});
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
->mountAction('setup_rbac')
->setActionData([
'role_definition_id' => 'role-1',
'role_display_name' => 'Policy and Profile Manager',
'scope' => 'all_devices',
'group_mode' => 'create',
])
->callMountedAction()
->assertHasNoActionErrors();
$tenant->refresh();
expect($tenant->rbac_group_id)->toBe('group-1');
expect($tenant->rbac_role_assignment_id)->toBe('assign-1');
expect($tenant->rbac_canary_results)->toMatchArray([
'deviceConfigurations' => 'ok',
'deviceCompliancePolicies' => 'ok',
]);
expect($tenant->rbac_last_warnings)->toContain('ca_canary_disabled');
expect(Cache::has($cacheKey))->toBeFalse();
});
test('rbac action is idempotent on rerun', function () {
$tenant = tenantWithApp();
$user = User::factory()->create();
$this->actingAs($user);
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
app()->bind(GraphClientInterface::class, function () {
return new class implements GraphClientInterface
{
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
if ($method === 'GET' && $path === 'servicePrincipals') {
return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]);
}
if ($method === 'POST' && str_contains($path, '/members/$ref')) {
return new GraphResponse(false, [], 400, [
[
'error' => [
'code' => 'Request_BadRequest',
'message' => 'One or more added object references already exist for the following modified objects',
],
],
]);
}
if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') {
return new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]);
}
if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['value' => [[
'id' => 'assign-1',
'members' => ['group-1'],
'resourceScopes' => ['/'],
'roleDefinition' => ['id' => 'role-1'],
]]]);
}
if ($method === 'PATCH' && str_contains($path, 'deviceManagement/roleAssignments/assign-1')) {
return new GraphResponse(true, ['id' => 'assign-1']);
}
if ($method === 'GET' && str_starts_with($path, 'groups/')) {
$id = str_replace('groups/', '', $path);
return new GraphResponse(true, ['id' => $id, 'displayName' => "Group {$id}"]);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) {
return new GraphResponse(true, ['value' => []]);
}
return new GraphResponse(true, ['value' => []]);
}
};
});
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
->mountAction('setup_rbac')
->setActionData([
'role_definition_id' => 'role-1',
'role_display_name' => 'Policy and Profile Manager',
'scope' => 'scope_group',
'scope_group_id' => 'group-scope',
'group_mode' => 'existing',
'existing_group_id' => 'group-1',
])
->callMountedAction()
->assertHasNoActionErrors();
$tenant->refresh();
expect($tenant->rbac_group_id)->toBe('group-1');
expect($tenant->rbac_role_assignment_id)->toBe('assign-1');
expect($tenant->rbac_scope_mode)->toBe('scope_group');
expect($tenant->rbac_last_warnings)->toContain('scope_limited');
expect($tenant->rbac_status)->toBe('ok');
});
test('existing group membership error from Graph json payload is treated idempotently', function () {
$tenant = tenantWithApp();
$user = User::factory()->create();
$this->actingAs($user);
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
app()->bind(GraphClientInterface::class, function () {
return new class implements GraphClientInterface
{
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
if ($method === 'GET' && $path === 'servicePrincipals') {
return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]);
}
if ($method === 'GET' && $path === 'groups' && str_contains($options['query']['$filter'] ?? '', 'displayName')) {
return new GraphResponse(true, ['value' => [['id' => 'group-1', 'displayName' => 'Existing Group']]]);
}
if ($method === 'POST' && str_contains($path, '/members/$ref')) {
return new GraphResponse(false, [], 400, [
[
'error' => [
'code' => 'Request_BadRequest',
'message' => 'One or more added object references already exist for the following modified objects',
],
],
]);
}
if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') {
return new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]);
}
if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'POST' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['id' => 'assign-1']);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) {
return new GraphResponse(true, ['value' => []]);
}
return new GraphResponse(true, ['value' => []]);
}
};
});
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
->mountAction('setup_rbac')
->setActionData([
'role_definition_id' => 'role-1',
'role_display_name' => 'Policy and Profile Manager',
'scope' => 'all_devices',
'group_mode' => 'existing',
'existing_group_id' => 'group-1',
])
->callMountedAction()
->assertHasNoActionErrors();
$tenant->refresh();
expect($tenant->rbac_group_id)->toBe('group-1');
expect($tenant->rbac_role_assignment_id)->toBe('assign-1');
expect($tenant->rbac_status)->toBe('ok');
});
test('group picker is disabled without delegated token', function () {
$tenant = tenantWithApp();
$user = User::factory()->create();
$this->actingAs($user);
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
->mountAction('setup_rbac')
->setActionData([
'group_mode' => 'existing',
])
->assertFormFieldDisabled('existing_group_id');
expect(\App\Filament\Resources\TenantResource::groupSearchHelper($tenant))->toBe('Login to search groups');
});
test('group picker toggles when switching modes', function () {
$tenant = tenantWithApp();
$user = User::factory()->create();
$this->actingAs($user);
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
->mountAction('setup_rbac')
->setActionData([
'group_mode' => 'existing',
])
->assertFormFieldVisible('existing_group_id')
->assertFormFieldHidden('group_name');
});
test('delegated group search returns options and persists selection', function () {
$tenant = tenantWithApp();
$user = User::factory()->create();
$this->actingAs($user);
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
app()->bind(GraphClientInterface::class, function () {
return new class implements GraphClientInterface
{
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
$filter = $options['query']['$filter'] ?? '';
if ($method === 'GET' && $path === 'servicePrincipals') {
return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]);
}
if ($method === 'GET' && $path === 'groups' && str_contains($filter, 'securityEnabled eq true')) {
return new GraphResponse(true, ['value' => [
['id' => 'group-123', 'displayName' => 'Ops Team'],
['id' => 'group-456', 'displayName' => 'Helpdesk'],
]]);
}
if ($method === 'GET' && str_starts_with($path, 'groups/')) {
$id = str_replace('groups/', '', $path);
return new GraphResponse(true, ['id' => $id, 'displayName' => 'Ops Team']);
}
if ($method === 'POST' && str_contains($path, '/members/$ref')) {
return new GraphResponse(true, []);
}
if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') {
return new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]);
}
if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'POST' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['id' => 'assign-1']);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) {
return new GraphResponse(true, ['value' => []]);
}
return new GraphResponse(true, ['value' => []]);
}
};
});
$options = \App\Filament\Resources\TenantResource::groupSearchOptions($tenant, 'Ops');
expect($options)->toHaveKey('group-123');
expect($options['group-123'])->toContain('Ops Team');
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
->mountAction('setup_rbac')
->setActionData([
'group_mode' => 'existing',
'role_definition_id' => 'role-1',
'role_display_name' => 'Policy and Profile Manager',
'scope' => 'all_devices',
'existing_group_id' => 'group-123',
])
->assertFormFieldEnabled('existing_group_id')
->callMountedAction()
->assertHasNoActionErrors();
$tenant->refresh();
expect($tenant->rbac_group_id)->toBe('group-123');
expect($tenant->rbac_role_assignment_id)->toBe('assign-1');
expect(Cache::has($cacheKey))->toBeFalse();
});
test('delegated role search returns options and persists role definition id', function () {
$tenant = tenantWithApp();
$user = User::factory()->create();
$this->actingAs($user);
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
app()->bind(GraphClientInterface::class, function () {
return new class implements GraphClientInterface
{
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
$filter = $options['query']['$filter'] ?? '';
if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') {
return new GraphResponse(true, ['value' => [
['id' => 'role-1', 'displayName' => 'Policy and Profile Manager'],
['id' => 'role-2', 'displayName' => 'Helpdesk Operator'],
]]);
}
if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions/role-1') {
return new GraphResponse(true, ['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']);
}
if ($method === 'GET' && $path === 'servicePrincipals') {
return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]);
}
if ($method === 'GET' && $path === 'groups' && str_contains($filter, 'displayName eq')) {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'POST' && $path === 'groups') {
return new GraphResponse(true, ['id' => 'group-1']);
}
if ($method === 'POST' && str_contains($path, '/members/$ref')) {
return new GraphResponse(true, []);
}
if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'POST' && $path === 'deviceManagement/roleAssignments') {
return new GraphResponse(true, ['id' => 'assign-1']);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) {
return new GraphResponse(true, ['value' => []]);
}
if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) {
return new GraphResponse(true, ['value' => []]);
}
return new GraphResponse(true, ['value' => []]);
}
};
});
$roles = \App\Filament\Resources\TenantResource::roleSearchOptions($tenant, 'Policy');
expect($roles)->toHaveKey('role-1');
expect($roles['role-1'])->toContain('Policy and Profile Manager');
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
->mountAction('setup_rbac')
->setActionData([
'role_definition_id' => 'role-1',
'role_display_name' => 'Policy and Profile Manager',
'scope' => 'all_devices',
'group_mode' => 'create',
])
->callMountedAction()
->assertHasNoActionErrors();
$tenant->refresh();
expect($tenant->rbac_role_definition_id)->toBe('role-1');
expect($tenant->rbac_role_display_name)->toBe('Policy and Profile Manager');
expect(Cache::has($cacheKey))->toBeFalse();
});