## Summary
- implement Spec 147 for workspace-first tenant selector and remembered tenant context enforcement
- harden canonical and tenant-bound route behavior so selected tenant mismatch stays informational
- fix drift finding subject fallback for workspace-safe RBAC identifiers and centralize finding subject resolution
## Testing
- vendor/bin/sail artisan test --compact tests/Feature/Filament/FindingViewRbacEvidenceTest.php tests/Feature/Findings/FindingsListDefaultsTest.php
- vendor/bin/sail bin pint --dirty --format agent
## Notes
- branch pushed at de0679cd8b
- includes the spec artifacts under specs/147-tenant-selector-remembered-context-enforcement/
Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #176
316 lines
11 KiB
PHP
316 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\FindingResource;
|
|
use App\Filament\Widgets\Dashboard\RecentDriftFindings;
|
|
use App\Models\Finding;
|
|
use App\Models\InventoryItem;
|
|
use App\Support\Baselines\BaselineSubjectKey;
|
|
use Filament\Facades\Filament;
|
|
use Livewire\Livewire;
|
|
|
|
it('renders shared summary badges and RBAC row states on the finding detail page', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$finding = findingViewRbacFinding($tenant);
|
|
|
|
$this->actingAs($user)
|
|
->get(FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('Intune RBAC Role Definition drift')
|
|
->assertSee('Metadata-only change')
|
|
->assertSee('2 changed')
|
|
->assertSee('1 added')
|
|
->assertSee('1 removed')
|
|
->assertSee('4 unchanged')
|
|
->assertSee('Changed')
|
|
->assertSee('Added')
|
|
->assertSee('Removed')
|
|
->assertSee('Unchanged')
|
|
->assertSee('Changed value')
|
|
->assertSee('No material change')
|
|
->assertSee('Role definition > Description')
|
|
->assertSee('Permission block 1 > Denied actions')
|
|
->assertSee('Permission block 1 > Conditions')
|
|
->assertSee('Baseline description')
|
|
->assertSee('Updated description')
|
|
->assertSee('border-warning-200')
|
|
->assertSee('text-gray-500')
|
|
->assertSee('Role Assignments are not included')
|
|
->assertSee('RBAC restore is not supported');
|
|
});
|
|
|
|
it('renders Allowed Actions as inline added removed and unchanged list chips', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$finding = findingViewRbacFinding($tenant);
|
|
|
|
$this->actingAs($user)
|
|
->get(FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('Permission block 1 > Allowed actions')
|
|
->assertSee('Added items')
|
|
->assertSee('Removed items')
|
|
->assertSee('Unchanged items')
|
|
->assertSee('Microsoft.Intune/deviceConfigurations/create')
|
|
->assertSee('Microsoft.Intune/deviceConfigurations/delete')
|
|
->assertSee('Microsoft.Intune/deviceConfigurations/read')
|
|
->assertSee('@Resource[Microsoft.Intune/deviceConfigurations] Exists')
|
|
->assertSee('Microsoft.Intune/deviceConfigurations/wipe');
|
|
});
|
|
|
|
it('renders a no-change RBAC summary when the evidence only contains unchanged rows', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$finding = findingViewRbacFinding($tenant, [
|
|
'changed_keys' => [],
|
|
'metadata_keys' => [],
|
|
'permission_keys' => [],
|
|
'baseline' => [
|
|
'normalized' => [
|
|
'Role definition > Display name' => 'Security Reader',
|
|
'Role definition > Description' => 'Baseline description',
|
|
],
|
|
'is_built_in' => false,
|
|
'role_permission_count' => 1,
|
|
],
|
|
'current' => [
|
|
'normalized' => [
|
|
'Role definition > Display name' => 'Security Reader',
|
|
'Role definition > Description' => 'Baseline description',
|
|
],
|
|
'is_built_in' => false,
|
|
'role_permission_count' => 1,
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('0 changed')
|
|
->assertSee('0 added')
|
|
->assertSee('0 removed')
|
|
->assertSee('4 unchanged')
|
|
->assertSee('No changes detected.');
|
|
});
|
|
|
|
it('renders a stable sparse fallback when the RBAC evidence payload is empty', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$finding = findingViewRbacFinding($tenant, []);
|
|
$evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
|
|
$evidence['rbac_role_definition'] = [];
|
|
|
|
$finding->forceFill([
|
|
'evidence_jsonb' => $evidence,
|
|
])->save();
|
|
|
|
$this->actingAs($user)
|
|
->get(FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('0 changed')
|
|
->assertSee('0 added')
|
|
->assertSee('0 removed')
|
|
->assertSee('0 unchanged')
|
|
->assertSee('No diff data available.')
|
|
->assertSee('RBAC restore is not supported');
|
|
});
|
|
|
|
it('shows RBAC labels and display-name fallback in the recent drift findings widget', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user);
|
|
|
|
$tenant->makeCurrent();
|
|
Filament::setCurrentPanel(Filament::getPanel('tenant'));
|
|
Filament::setTenant($tenant, true);
|
|
|
|
$rawSubjectExternalId = 'rbac-role-1';
|
|
$subjectExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalIdForPolicy(
|
|
'intuneRoleDefinition',
|
|
'Security Reader',
|
|
$rawSubjectExternalId,
|
|
);
|
|
|
|
InventoryItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'external_id' => $rawSubjectExternalId,
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'display_name' => 'Security Reader',
|
|
'meta_jsonb' => ['etag' => 'E1'],
|
|
'last_seen_at' => now(),
|
|
]);
|
|
|
|
$finding = Finding::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'source' => 'baseline.compare',
|
|
'subject_type' => 'policy',
|
|
'subject_external_id' => (string) $subjectExternalId,
|
|
'severity' => Finding::SEVERITY_HIGH,
|
|
'status' => Finding::STATUS_NEW,
|
|
'evidence_jsonb' => [
|
|
'change_type' => 'missing_policy',
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'subject_key' => hash('sha256', 'intuneRoleDefinition|rbac-role-1'),
|
|
'display_name' => 'Security Reader',
|
|
'summary' => [
|
|
'kind' => 'rbac_role_definition',
|
|
],
|
|
'baseline' => ['policy_version_id' => 10],
|
|
'current' => ['policy_version_id' => null],
|
|
'rbac_role_definition' => [
|
|
'diff_kind' => 'missing',
|
|
],
|
|
'fidelity' => 'mixed',
|
|
'provenance' => [
|
|
'baseline_profile_id' => 1,
|
|
'baseline_snapshot_id' => 1,
|
|
'compare_operation_run_id' => 1,
|
|
'inventory_sync_run_id' => null,
|
|
],
|
|
],
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(RecentDriftFindings::class)
|
|
->assertCanSeeTableRecords([$finding])
|
|
->assertSee('Security Reader')
|
|
->assertSee('Intune RBAC Role Definition drift');
|
|
});
|
|
|
|
/**
|
|
* @param array<string, mixed> $rbacOverrides
|
|
*/
|
|
function findingViewRbacFinding(\App\Models\Tenant $tenant, array $rbacOverrides = []): Finding
|
|
{
|
|
$subjectExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalIdForPolicy(
|
|
'intuneRoleDefinition',
|
|
'Security Reader',
|
|
'rbac-role-1',
|
|
);
|
|
|
|
return Finding::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'source' => 'baseline.compare',
|
|
'subject_type' => 'policy',
|
|
'subject_external_id' => (string) $subjectExternalId,
|
|
'evidence_fidelity' => 'content',
|
|
'severity' => Finding::SEVERITY_LOW,
|
|
'evidence_jsonb' => [
|
|
'change_type' => 'different_version',
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'subject_key' => hash('sha256', 'intuneRoleDefinition|rbac-role-1'),
|
|
'display_name' => 'Security Reader',
|
|
'summary' => [
|
|
'kind' => 'rbac_role_definition',
|
|
],
|
|
'baseline' => [
|
|
'policy_version_id' => 10,
|
|
'hash' => 'baseline',
|
|
],
|
|
'current' => [
|
|
'policy_version_id' => 11,
|
|
'hash' => 'current',
|
|
],
|
|
'rbac_role_definition' => findingViewRbacEvidenceFixture($rbacOverrides),
|
|
'fidelity' => 'content',
|
|
'provenance' => [
|
|
'baseline_profile_id' => 1,
|
|
'baseline_snapshot_id' => 1,
|
|
'compare_operation_run_id' => 1,
|
|
'inventory_sync_run_id' => 1,
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
* @return array<string, mixed>
|
|
*/
|
|
function findingViewRbacEvidenceFixture(array $overrides = []): array
|
|
{
|
|
return findingViewRbacFixtureMerge([
|
|
'diff_kind' => 'metadata_only',
|
|
'diff_fingerprint' => 'rbac-diff-1',
|
|
'changed_keys' => [
|
|
'Role definition > Description',
|
|
'Permission block 1 > Allowed actions',
|
|
],
|
|
'metadata_keys' => [
|
|
'Role definition > Description',
|
|
],
|
|
'permission_keys' => [
|
|
'Permission block 1 > Allowed actions',
|
|
],
|
|
'baseline' => [
|
|
'normalized' => [
|
|
'Role definition > Display name' => 'Security Reader',
|
|
'Role definition > Description' => 'Baseline description',
|
|
'Role definition > Scope tag IDs' => ['0', 'scope-1'],
|
|
'Permission block 1 > Allowed actions' => [
|
|
'Microsoft.Intune/deviceConfigurations/delete',
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
],
|
|
'Permission block 1 > Denied actions' => [
|
|
'Microsoft.Intune/deviceConfigurations/wipe',
|
|
],
|
|
],
|
|
'is_built_in' => false,
|
|
'role_permission_count' => 1,
|
|
],
|
|
'current' => [
|
|
'normalized' => [
|
|
'Role definition > Display name' => 'Security Reader',
|
|
'Role definition > Description' => 'Updated description',
|
|
'Role definition > Scope tag IDs' => ['0', 'scope-1'],
|
|
'Permission block 1 > Allowed actions' => [
|
|
'Microsoft.Intune/deviceConfigurations/create',
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
],
|
|
'Permission block 1 > Conditions' => [
|
|
'@Resource[Microsoft.Intune/deviceConfigurations] Exists',
|
|
],
|
|
],
|
|
'is_built_in' => false,
|
|
'role_permission_count' => 1,
|
|
],
|
|
], $overrides);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $base
|
|
* @param array<string, mixed> $overrides
|
|
* @return array<string, mixed>
|
|
*/
|
|
function findingViewRbacFixtureMerge(array $base, array $overrides): array
|
|
{
|
|
foreach ($overrides as $key => $value) {
|
|
if ($key === 'normalized') {
|
|
$base[$key] = $value;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
is_string($key)
|
|
&& array_key_exists($key, $base)
|
|
&& is_array($value)
|
|
&& is_array($base[$key])
|
|
&& ! array_is_list($value)
|
|
&& ! array_is_list($base[$key])
|
|
) {
|
|
$base[$key] = findingViewRbacFixtureMerge($base[$key], $value);
|
|
|
|
continue;
|
|
}
|
|
|
|
$base[$key] = $value;
|
|
}
|
|
|
|
return $base;
|
|
}
|