feat: move assignment options to capture actions

This commit is contained in:
Ahmed Darrazi 2025-12-22 21:36:04 +01:00
parent c3bdcf4d2d
commit 2269904857
7 changed files with 151 additions and 41 deletions

View File

@ -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,
);
}
}

View File

@ -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()

View File

@ -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.')

View File

@ -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',

View File

@ -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,
);
}

View File

@ -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',

View File

@ -0,0 +1,55 @@
<?php
use App\Filament\Resources\PolicyResource\Pages\ViewPolicy;
use App\Models\Policy;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Graph\AssignmentFetcher;
use App\Services\Intune\PolicySnapshotService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery\MockInterface;
uses(RefreshDatabase::class);
it('captures a policy snapshot with scope tags when requested', function () {
$tenant = Tenant::factory()->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'],
]);
});