From 2269904857ded4e30f3c53ceb9100ff9def5aa61 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Mon, 22 Dec 2025 21:36:04 +0100 Subject: [PATCH] feat: move assignment options to capture actions --- app/Filament/Resources/BackupSetResource.php | 5 +- .../BackupItemsRelationManager.php | 12 +++- .../PolicyResource/Pages/ViewPolicy.php | 21 ++++++- app/Services/Intune/BackupService.php | 26 ++++++-- app/Services/Intune/VersionService.php | 62 ++++++++++--------- tests/Feature/Filament/BackupCreationTest.php | 11 ++++ .../PolicyCaptureSnapshotOptionsTest.php | 55 ++++++++++++++++ 7 files changed, 151 insertions(+), 41 deletions(-) create mode 100644 tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php diff --git a/app/Filament/Resources/BackupSetResource.php b/app/Filament/Resources/BackupSetResource.php index 3f23506..e133010 100644 --- a/app/Filament/Resources/BackupSetResource.php +++ b/app/Filament/Resources/BackupSetResource.php @@ -36,10 +36,6 @@ public static function form(Schema $schema): Schema ->label('Backup name') ->default(fn () => now()->format('Y-m-d H:i:s').' backup') ->required(), - Forms\Components\Checkbox::make('include_assignments') - ->label('Include Assignments & Scope Tags') - ->helperText('Captures group/user targeting and RBAC scope. Adds ~2-5 KB per policy.') - ->default(false), ]); } @@ -202,6 +198,7 @@ public static function createBackupSet(array $data): BackupSet actorEmail: auth()->user()?->email, actorName: auth()->user()?->name, includeAssignments: $data['include_assignments'] ?? false, + includeScopeTags: $data['include_scope_tags'] ?? false, ); } } diff --git a/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php b/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php index a3ee34b..90200f3 100644 --- a/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php +++ b/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php @@ -59,12 +59,13 @@ public function table(Table $table): Table ->default('—') ->formatStateUsing(function ($state, BackupItem $record) { // Get scope tags from PolicyVersion if available - if ($record->policyVersion && !empty($record->policyVersion->scope_tags)) { + if ($record->policyVersion && ! empty($record->policyVersion->scope_tags)) { $tags = $record->policyVersion->scope_tags; if (is_array($tags) && isset($tags['names'])) { return implode(', ', $tags['names']); } } + return '—'; }), Tables\Columns\TextColumn::make('captured_at')->dateTime(), @@ -97,9 +98,13 @@ public function table(Table $table): Table ->pluck('display_name', 'id'); }), Forms\Components\Checkbox::make('include_assignments') - ->label('Include Assignments') + ->label('Include assignments') ->default(true) - ->helperText('Capture policy assignments and scope tags'), + ->helperText('Captures assignment include/exclude targeting and filters.'), + Forms\Components\Checkbox::make('include_scope_tags') + ->label('Include scope tags') + ->default(true) + ->helperText('Captures policy scope tag IDs.'), ]) ->action(function (array $data, BackupService $service) { if (empty($data['policy_ids'])) { @@ -121,6 +126,7 @@ public function table(Table $table): Table actorEmail: auth()->user()?->email, actorName: auth()->user()?->name, includeAssignments: $data['include_assignments'] ?? false, + includeScopeTags: $data['include_scope_tags'] ?? false, ); Notification::make() diff --git a/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php b/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php index ad2188f..17c7b1b 100644 --- a/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php +++ b/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php @@ -5,6 +5,7 @@ use App\Filament\Resources\PolicyResource; use App\Services\Intune\VersionService; use Filament\Actions\Action; +use Filament\Forms; use Filament\Notifications\Notification; use Filament\Resources\Pages\ViewRecord; use Filament\Support\Enums\Width; @@ -23,7 +24,17 @@ protected function getActions(): array ->requiresConfirmation() ->modalHeading('Capture snapshot now') ->modalSubheading('This will fetch the latest configuration from Microsoft Graph and store a new policy version.') - ->action(function () { + ->form([ + Forms\Components\Checkbox::make('include_assignments') + ->label('Include assignments') + ->default(true) + ->helperText('Captures assignment include/exclude targeting and filters.'), + Forms\Components\Checkbox::make('include_scope_tags') + ->label('Include scope tags') + ->default(true) + ->helperText('Captures policy scope tag IDs.'), + ]) + ->action(function (array $data) { $policy = $this->record; try { @@ -38,7 +49,13 @@ protected function getActions(): array return; } - app(VersionService::class)->captureFromGraph($tenant, $policy, auth()->user()?->email ?? null); + app(VersionService::class)->captureFromGraph( + tenant: $tenant, + policy: $policy, + createdBy: auth()->user()?->email ?? null, + includeAssignments: $data['include_assignments'] ?? false, + includeScopeTags: $data['include_scope_tags'] ?? false, + ); Notification::make() ->title('Snapshot captured successfully.') diff --git a/app/Services/Intune/BackupService.php b/app/Services/Intune/BackupService.php index 72b5ed7..dd8aee4 100644 --- a/app/Services/Intune/BackupService.php +++ b/app/Services/Intune/BackupService.php @@ -33,6 +33,7 @@ public function createBackupSet( ?string $actorName = null, ?string $name = null, bool $includeAssignments = false, + bool $includeScopeTags = false, ): BackupSet { $this->assertActiveTenant($tenant); @@ -41,7 +42,7 @@ public function createBackupSet( ->whereIn('id', $policyIds) ->get(); - $backupSet = DB::transaction(function () use ($tenant, $policies, $actorEmail, $name, $includeAssignments) { + $backupSet = DB::transaction(function () use ($tenant, $policies, $actorEmail, $name, $includeAssignments, $includeScopeTags) { $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, 'name' => $name ?? CarbonImmutable::now()->format('Y-m-d H:i:s').' backup', @@ -54,7 +55,14 @@ public function createBackupSet( $itemsCreated = 0; foreach ($policies as $policy) { - [$item, $failure] = $this->snapshotPolicy($tenant, $backupSet, $policy, $actorEmail, $includeAssignments); + [$item, $failure] = $this->snapshotPolicy( + $tenant, + $backupSet, + $policy, + $actorEmail, + $includeAssignments, + $includeScopeTags + ); if ($failure !== null) { $failures[] = $failure; @@ -136,6 +144,7 @@ public function addPoliciesToSet( ?string $actorEmail = null, ?string $actorName = null, bool $includeAssignments = false, + bool $includeScopeTags = false, ): BackupSet { $this->assertActiveTenant($tenant); @@ -171,7 +180,14 @@ public function addPoliciesToSet( $itemsCreated = 0; foreach ($policies as $policy) { - [$item, $failure] = $this->snapshotPolicy($tenant, $backupSet, $policy, $actorEmail, $includeAssignments); + [$item, $failure] = $this->snapshotPolicy( + $tenant, + $backupSet, + $policy, + $actorEmail, + $includeAssignments, + $includeScopeTags + ); if ($failure !== null) { $failures[] = $failure; @@ -231,13 +247,15 @@ private function snapshotPolicy( BackupSet $backupSet, Policy $policy, ?string $actorEmail = null, - bool $includeAssignments = false + bool $includeAssignments = false, + bool $includeScopeTags = false ): array { // Use orchestrator to capture policy + assignments into PolicyVersion first $captureResult = $this->captureOrchestrator->capture( policy: $policy, tenant: $tenant, includeAssignments: $includeAssignments, + includeScopeTags: $includeScopeTags, createdBy: $actorEmail, metadata: [ 'source' => 'backup', diff --git a/app/Services/Intune/VersionService.php b/app/Services/Intune/VersionService.php index 9597455..6c4984e 100644 --- a/app/Services/Intune/VersionService.php +++ b/app/Services/Intune/VersionService.php @@ -66,6 +66,8 @@ public function captureFromGraph( Policy $policy, ?string $createdBy = null, array $metadata = [], + bool $includeAssignments = true, + bool $includeScopeTags = true, ): PolicyVersion { $snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy); @@ -75,38 +77,42 @@ public function captureFromGraph( throw new \RuntimeException($reason); } - // Fetch assignments from Graph - $assignments = []; - $scopeTags = []; + $payload = $snapshot['payload']; + $assignments = null; + $scopeTags = null; $assignmentMetadata = []; - try { - $rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id); + if ($includeAssignments) { + try { + $rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id); - if (! empty($rawAssignments)) { - $assignments = $rawAssignments; + if (! empty($rawAssignments)) { + $assignments = $rawAssignments; - // Resolve groups and scope tags - $groupIds = collect($rawAssignments) - ->pluck('target.groupId') - ->filter() - ->unique() - ->values() - ->toArray(); + // Resolve groups + $groupIds = collect($rawAssignments) + ->pluck('target.groupId') + ->filter() + ->unique() + ->values() + ->toArray(); - $resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds); + $resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds); - $scopeTags = [ - 'ids' => $policy->roleScopeTagIds ?? ['0'], - 'names' => ['Default'], // Could be fetched from Graph if needed - ]; - - $assignmentMetadata['has_orphaned_assignments'] = ! empty($resolvedGroups['orphaned']); - $assignmentMetadata['assignments_count'] = count($rawAssignments); + $assignmentMetadata['has_orphaned_assignments'] = ! empty($resolvedGroups['orphaned']); + $assignmentMetadata['assignments_count'] = count($rawAssignments); + } + } catch (\Throwable $e) { + $assignmentMetadata['assignments_fetch_failed'] = true; + $assignmentMetadata['assignments_fetch_error'] = $e->getMessage(); } - } catch (\Throwable $e) { - $assignmentMetadata['assignments_fetch_failed'] = true; - $assignmentMetadata['assignments_fetch_error'] = $e->getMessage(); + } + + if ($includeScopeTags) { + $scopeTags = [ + 'ids' => $payload['roleScopeTagIds'] ?? ['0'], + 'names' => ['Default'], // Could be fetched from Graph if needed + ]; } $metadata = array_merge( @@ -117,11 +123,11 @@ public function captureFromGraph( return $this->captureVersion( policy: $policy, - payload: $snapshot['payload'], + payload: $payload, createdBy: $createdBy, metadata: $metadata, - assignments: ! empty($assignments) ? $assignments : null, - scopeTags: ! empty($scopeTags) ? $scopeTags : null, + assignments: $assignments, + scopeTags: $scopeTags, ); } diff --git a/tests/Feature/Filament/BackupCreationTest.php b/tests/Feature/Filament/BackupCreationTest.php index 69b2dc0..896b6f3 100644 --- a/tests/Feature/Filament/BackupCreationTest.php +++ b/tests/Feature/Filament/BackupCreationTest.php @@ -2,6 +2,7 @@ use App\Filament\Resources\BackupSetResource; use App\Models\Policy; +use App\Models\PolicyVersion; use App\Models\Tenant; use App\Models\User; use App\Services\Graph\GraphClientInterface; @@ -109,6 +110,8 @@ public function request(string $method, string $path, array $options = []): Grap 'pageClass' => \App\Filament\Resources\BackupSetResource\Pages\ViewBackupSet::class, ])->callTableAction('addPolicies', data: [ 'policy_ids' => [$policyA->id, $policyB->id], + 'include_assignments' => false, + 'include_scope_tags' => true, ]); $backupSet->refresh(); @@ -117,6 +120,14 @@ public function request(string $method, string $path, array $options = []): Grap expect($backupSet->items)->toHaveCount(2); expect($backupSet->items->first()->payload['id'])->toBe('policy-1'); + $firstVersion = PolicyVersion::find($backupSet->items->first()->policy_version_id); + expect($firstVersion)->not->toBeNull(); + expect($firstVersion->scope_tags)->toBe([ + 'ids' => ['0'], + 'names' => ['Default'], + ]); + expect($firstVersion->assignments)->toBeNull(); + $this->assertDatabaseHas('audit_logs', [ 'action' => 'backup.created', 'resource_type' => 'backup_set', diff --git a/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php b/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php new file mode 100644 index 0000000..abd8857 --- /dev/null +++ b/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php @@ -0,0 +1,55 @@ +create(); + $tenant->makeCurrent(); + $policy = Policy::factory()->for($tenant)->create([ + 'external_id' => 'policy-123', + ]); + + $user = User::factory()->create(); + $this->actingAs($user); + + $this->mock(PolicySnapshotService::class, function (MockInterface $mock) use ($policy) { + $mock->shouldReceive('fetch') + ->once() + ->andReturn([ + 'payload' => [ + 'id' => $policy->external_id, + 'name' => $policy->display_name, + 'roleScopeTagIds' => ['0'], + ], + ]); + }); + + $this->mock(AssignmentFetcher::class, function (MockInterface $mock) { + $mock->shouldReceive('fetch')->never(); + }); + + Livewire::test(ViewPolicy::class, ['record' => $policy->getRouteKey()]) + ->callAction('capture_snapshot', data: [ + 'include_assignments' => false, + 'include_scope_tags' => true, + ]); + + $version = $policy->versions()->first(); + + expect($version)->not->toBeNull(); + expect($version->assignments)->toBeNull(); + expect($version->scope_tags)->toBe([ + 'ids' => ['0'], + 'names' => ['Default'], + ]); +});