## Summary - add Intune RBAC role definitions and role assignments as foundation-backed inventory, backup, and versioned snapshot types - add RBAC-specific normalization, coverage, permission-warning handling, and preview-only restore safety behavior across existing Filament and service surfaces - add spec 127 artifacts, contracts, audits, and focused regression coverage for inventory, backup, versioning, verification, and authorization behavior ## Testing - `vendor/bin/sail bin pint --dirty --format agent` - `vendor/bin/sail artisan test --compact tests/Feature/Inventory/InventorySyncServiceTest.php tests/Feature/Filament/InventoryCoverageTableTest.php tests/Feature/FoundationBackupTest.php tests/Feature/Filament/RestoreExecutionTest.php tests/Feature/RestoreUnknownPolicyTypeSafetyTest.php tests/Unit/GraphContractRegistryTest.php tests/Unit/FoundationSnapshotServiceTest.php tests/Feature/Verification/IntuneRbacPermissionCoverageTest.php tests/Unit/IntuneRoleDefinitionNormalizerTest.php tests/Unit/IntuneRoleAssignmentNormalizerTest.php` ## Notes - tasks in `specs/127-rbac-inventory-backup/tasks.md` are complete except `T041`, which is the documented manual QA validation step Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #155
182 lines
6.6 KiB
PHP
182 lines
6.6 KiB
PHP
<?php
|
|
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSchedule;
|
|
use App\Models\BackupSet;
|
|
use App\Models\EntraGroup;
|
|
use App\Models\EntraRoleDefinition;
|
|
use App\Models\Finding;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\InventoryLink;
|
|
use App\Models\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\RestoreRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantPermission;
|
|
use App\Models\Workspace;
|
|
use App\Support\WorkspaceIsolation\WorkspaceIsolationViolation;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('enforces tenant workspace binding for tenant-owned models even when events are disabled', function (): void {
|
|
$workspaceA = Workspace::factory()->create();
|
|
$workspaceB = Workspace::factory()->create();
|
|
|
|
$tenant = Tenant::factory()->create([
|
|
'workspace_id' => $workspaceA->getKey(),
|
|
]);
|
|
|
|
$policy = Policy::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceA->getKey(),
|
|
]);
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceA->getKey(),
|
|
]);
|
|
|
|
$cases = [
|
|
'policies' => fn (int $workspaceId) => Policy::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
]),
|
|
'policy_versions' => fn (int $workspaceId) => PolicyVersion::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
'policy_id' => $policy->getKey(),
|
|
]),
|
|
'backup_sets' => fn (int $workspaceId) => BackupSet::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
]),
|
|
'backup_items' => fn (int $workspaceId) => BackupItem::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
'backup_set_id' => $backupSet->getKey(),
|
|
'policy_id' => $policy->getKey(),
|
|
]),
|
|
'restore_runs' => fn (int $workspaceId) => RestoreRun::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
'backup_set_id' => $backupSet->getKey(),
|
|
]),
|
|
'backup_schedules' => fn (int $workspaceId) => BackupSchedule::make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
'name' => 'Weekly backup',
|
|
'is_enabled' => true,
|
|
'timezone' => 'UTC',
|
|
'frequency' => 'daily',
|
|
'time_of_day' => '00:00:00',
|
|
'days_of_week' => null,
|
|
'policy_types' => ['settingsCatalogPolicy'],
|
|
'include_foundations' => true,
|
|
'retention_keep_last' => 30,
|
|
]),
|
|
'inventory_items' => fn (int $workspaceId) => InventoryItem::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
]),
|
|
'inventory_links' => fn (int $workspaceId) => InventoryLink::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
]),
|
|
'entra_groups' => fn (int $workspaceId) => EntraGroup::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
]),
|
|
'findings' => fn (int $workspaceId) => Finding::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
]),
|
|
'entra_role_definitions' => fn (int $workspaceId) => EntraRoleDefinition::factory()->make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
]),
|
|
'tenant_permissions' => fn (int $workspaceId) => TenantPermission::make([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceId,
|
|
'permission_key' => 'test.permission.'.uniqid(),
|
|
'status' => 'missing',
|
|
]),
|
|
];
|
|
|
|
foreach ($cases as $table => $makeModel) {
|
|
$model = $makeModel((int) $workspaceA->getKey());
|
|
|
|
($model::class)::withoutEvents(function () use ($model): void {
|
|
$model->save();
|
|
});
|
|
|
|
$this->assertDatabaseHas($table, [
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspaceA->getKey(),
|
|
]);
|
|
|
|
$mismatched = $makeModel((int) $workspaceB->getKey());
|
|
|
|
expect(fn () => $mismatched->save())
|
|
->toThrow(WorkspaceIsolationViolation::class);
|
|
}
|
|
});
|
|
|
|
it('keeps RBAC foundation policy versions and backup items pinned to the tenant workspace', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
$tenant = Tenant::factory()->create([
|
|
'workspace_id' => $workspace->getKey(),
|
|
]);
|
|
|
|
$policy = Policy::query()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspace->getKey(),
|
|
'external_id' => 'role-def-1',
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'platform' => 'all',
|
|
'display_name' => 'Policy and Profile Manager',
|
|
'last_synced_at' => null,
|
|
'metadata' => ['foundation_anchor' => true],
|
|
]);
|
|
|
|
$version = PolicyVersion::query()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspace->getKey(),
|
|
'policy_id' => $policy->getKey(),
|
|
'version_number' => 1,
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'platform' => 'all',
|
|
'captured_at' => now(),
|
|
'snapshot' => ['displayName' => 'Policy and Profile Manager'],
|
|
'metadata' => ['capture_source' => 'foundation_capture'],
|
|
'secret_fingerprints' => [
|
|
'snapshot' => [],
|
|
'assignments' => [],
|
|
'scope_tags' => [],
|
|
],
|
|
'redaction_version' => 1,
|
|
]);
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspace->getKey(),
|
|
]);
|
|
|
|
$backupItem = BackupItem::query()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'workspace_id' => $workspace->getKey(),
|
|
'backup_set_id' => $backupSet->getKey(),
|
|
'policy_id' => $policy->getKey(),
|
|
'policy_version_id' => $version->getKey(),
|
|
'policy_identifier' => 'role-def-1',
|
|
'policy_type' => 'intuneRoleDefinition',
|
|
'platform' => 'all',
|
|
'payload' => ['displayName' => 'Policy and Profile Manager'],
|
|
'metadata' => ['displayName' => 'Policy and Profile Manager'],
|
|
]);
|
|
|
|
expect((int) $version->workspace_id)->toBe((int) $workspace->getKey());
|
|
expect((int) $backupItem->workspace_id)->toBe((int) $workspace->getKey());
|
|
});
|