feat: move assignment options to capture actions
This commit is contained in:
parent
c3bdcf4d2d
commit
2269904857
@ -36,10 +36,6 @@ public static function form(Schema $schema): Schema
|
|||||||
->label('Backup name')
|
->label('Backup name')
|
||||||
->default(fn () => now()->format('Y-m-d H:i:s').' backup')
|
->default(fn () => now()->format('Y-m-d H:i:s').' backup')
|
||||||
->required(),
|
->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,
|
actorEmail: auth()->user()?->email,
|
||||||
actorName: auth()->user()?->name,
|
actorName: auth()->user()?->name,
|
||||||
includeAssignments: $data['include_assignments'] ?? false,
|
includeAssignments: $data['include_assignments'] ?? false,
|
||||||
|
includeScopeTags: $data['include_scope_tags'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,12 +59,13 @@ public function table(Table $table): Table
|
|||||||
->default('—')
|
->default('—')
|
||||||
->formatStateUsing(function ($state, BackupItem $record) {
|
->formatStateUsing(function ($state, BackupItem $record) {
|
||||||
// Get scope tags from PolicyVersion if available
|
// 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;
|
$tags = $record->policyVersion->scope_tags;
|
||||||
if (is_array($tags) && isset($tags['names'])) {
|
if (is_array($tags) && isset($tags['names'])) {
|
||||||
return implode(', ', $tags['names']);
|
return implode(', ', $tags['names']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '—';
|
return '—';
|
||||||
}),
|
}),
|
||||||
Tables\Columns\TextColumn::make('captured_at')->dateTime(),
|
Tables\Columns\TextColumn::make('captured_at')->dateTime(),
|
||||||
@ -97,9 +98,13 @@ public function table(Table $table): Table
|
|||||||
->pluck('display_name', 'id');
|
->pluck('display_name', 'id');
|
||||||
}),
|
}),
|
||||||
Forms\Components\Checkbox::make('include_assignments')
|
Forms\Components\Checkbox::make('include_assignments')
|
||||||
->label('Include Assignments')
|
->label('Include assignments')
|
||||||
->default(true)
|
->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) {
|
->action(function (array $data, BackupService $service) {
|
||||||
if (empty($data['policy_ids'])) {
|
if (empty($data['policy_ids'])) {
|
||||||
@ -121,6 +126,7 @@ public function table(Table $table): Table
|
|||||||
actorEmail: auth()->user()?->email,
|
actorEmail: auth()->user()?->email,
|
||||||
actorName: auth()->user()?->name,
|
actorName: auth()->user()?->name,
|
||||||
includeAssignments: $data['include_assignments'] ?? false,
|
includeAssignments: $data['include_assignments'] ?? false,
|
||||||
|
includeScopeTags: $data['include_scope_tags'] ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Filament\Resources\PolicyResource;
|
use App\Filament\Resources\PolicyResource;
|
||||||
use App\Services\Intune\VersionService;
|
use App\Services\Intune\VersionService;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Forms;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
use Filament\Support\Enums\Width;
|
use Filament\Support\Enums\Width;
|
||||||
@ -23,7 +24,17 @@ protected function getActions(): array
|
|||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->modalHeading('Capture snapshot now')
|
->modalHeading('Capture snapshot now')
|
||||||
->modalSubheading('This will fetch the latest configuration from Microsoft Graph and store a new policy version.')
|
->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;
|
$policy = $this->record;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -38,7 +49,13 @@ protected function getActions(): array
|
|||||||
return;
|
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()
|
Notification::make()
|
||||||
->title('Snapshot captured successfully.')
|
->title('Snapshot captured successfully.')
|
||||||
|
|||||||
@ -33,6 +33,7 @@ public function createBackupSet(
|
|||||||
?string $actorName = null,
|
?string $actorName = null,
|
||||||
?string $name = null,
|
?string $name = null,
|
||||||
bool $includeAssignments = false,
|
bool $includeAssignments = false,
|
||||||
|
bool $includeScopeTags = false,
|
||||||
): BackupSet {
|
): BackupSet {
|
||||||
$this->assertActiveTenant($tenant);
|
$this->assertActiveTenant($tenant);
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ public function createBackupSet(
|
|||||||
->whereIn('id', $policyIds)
|
->whereIn('id', $policyIds)
|
||||||
->get();
|
->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([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'name' => $name ?? CarbonImmutable::now()->format('Y-m-d H:i:s').' backup',
|
'name' => $name ?? CarbonImmutable::now()->format('Y-m-d H:i:s').' backup',
|
||||||
@ -54,7 +55,14 @@ public function createBackupSet(
|
|||||||
$itemsCreated = 0;
|
$itemsCreated = 0;
|
||||||
|
|
||||||
foreach ($policies as $policy) {
|
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) {
|
if ($failure !== null) {
|
||||||
$failures[] = $failure;
|
$failures[] = $failure;
|
||||||
@ -136,6 +144,7 @@ public function addPoliciesToSet(
|
|||||||
?string $actorEmail = null,
|
?string $actorEmail = null,
|
||||||
?string $actorName = null,
|
?string $actorName = null,
|
||||||
bool $includeAssignments = false,
|
bool $includeAssignments = false,
|
||||||
|
bool $includeScopeTags = false,
|
||||||
): BackupSet {
|
): BackupSet {
|
||||||
$this->assertActiveTenant($tenant);
|
$this->assertActiveTenant($tenant);
|
||||||
|
|
||||||
@ -171,7 +180,14 @@ public function addPoliciesToSet(
|
|||||||
$itemsCreated = 0;
|
$itemsCreated = 0;
|
||||||
|
|
||||||
foreach ($policies as $policy) {
|
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) {
|
if ($failure !== null) {
|
||||||
$failures[] = $failure;
|
$failures[] = $failure;
|
||||||
@ -231,13 +247,15 @@ private function snapshotPolicy(
|
|||||||
BackupSet $backupSet,
|
BackupSet $backupSet,
|
||||||
Policy $policy,
|
Policy $policy,
|
||||||
?string $actorEmail = null,
|
?string $actorEmail = null,
|
||||||
bool $includeAssignments = false
|
bool $includeAssignments = false,
|
||||||
|
bool $includeScopeTags = false
|
||||||
): array {
|
): array {
|
||||||
// Use orchestrator to capture policy + assignments into PolicyVersion first
|
// Use orchestrator to capture policy + assignments into PolicyVersion first
|
||||||
$captureResult = $this->captureOrchestrator->capture(
|
$captureResult = $this->captureOrchestrator->capture(
|
||||||
policy: $policy,
|
policy: $policy,
|
||||||
tenant: $tenant,
|
tenant: $tenant,
|
||||||
includeAssignments: $includeAssignments,
|
includeAssignments: $includeAssignments,
|
||||||
|
includeScopeTags: $includeScopeTags,
|
||||||
createdBy: $actorEmail,
|
createdBy: $actorEmail,
|
||||||
metadata: [
|
metadata: [
|
||||||
'source' => 'backup',
|
'source' => 'backup',
|
||||||
|
|||||||
@ -66,6 +66,8 @@ public function captureFromGraph(
|
|||||||
Policy $policy,
|
Policy $policy,
|
||||||
?string $createdBy = null,
|
?string $createdBy = null,
|
||||||
array $metadata = [],
|
array $metadata = [],
|
||||||
|
bool $includeAssignments = true,
|
||||||
|
bool $includeScopeTags = true,
|
||||||
): PolicyVersion {
|
): PolicyVersion {
|
||||||
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
|
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
|
||||||
|
|
||||||
@ -75,38 +77,42 @@ public function captureFromGraph(
|
|||||||
throw new \RuntimeException($reason);
|
throw new \RuntimeException($reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch assignments from Graph
|
$payload = $snapshot['payload'];
|
||||||
$assignments = [];
|
$assignments = null;
|
||||||
$scopeTags = [];
|
$scopeTags = null;
|
||||||
$assignmentMetadata = [];
|
$assignmentMetadata = [];
|
||||||
|
|
||||||
try {
|
if ($includeAssignments) {
|
||||||
$rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id);
|
try {
|
||||||
|
$rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id);
|
||||||
|
|
||||||
if (! empty($rawAssignments)) {
|
if (! empty($rawAssignments)) {
|
||||||
$assignments = $rawAssignments;
|
$assignments = $rawAssignments;
|
||||||
|
|
||||||
// Resolve groups and scope tags
|
// Resolve groups
|
||||||
$groupIds = collect($rawAssignments)
|
$groupIds = collect($rawAssignments)
|
||||||
->pluck('target.groupId')
|
->pluck('target.groupId')
|
||||||
->filter()
|
->filter()
|
||||||
->unique()
|
->unique()
|
||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
$resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds);
|
$resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds);
|
||||||
|
|
||||||
$scopeTags = [
|
$assignmentMetadata['has_orphaned_assignments'] = ! empty($resolvedGroups['orphaned']);
|
||||||
'ids' => $policy->roleScopeTagIds ?? ['0'],
|
$assignmentMetadata['assignments_count'] = count($rawAssignments);
|
||||||
'names' => ['Default'], // Could be fetched from Graph if needed
|
}
|
||||||
];
|
} catch (\Throwable $e) {
|
||||||
|
$assignmentMetadata['assignments_fetch_failed'] = true;
|
||||||
$assignmentMetadata['has_orphaned_assignments'] = ! empty($resolvedGroups['orphaned']);
|
$assignmentMetadata['assignments_fetch_error'] = $e->getMessage();
|
||||||
$assignmentMetadata['assignments_count'] = count($rawAssignments);
|
|
||||||
}
|
}
|
||||||
} 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(
|
$metadata = array_merge(
|
||||||
@ -117,11 +123,11 @@ public function captureFromGraph(
|
|||||||
|
|
||||||
return $this->captureVersion(
|
return $this->captureVersion(
|
||||||
policy: $policy,
|
policy: $policy,
|
||||||
payload: $snapshot['payload'],
|
payload: $payload,
|
||||||
createdBy: $createdBy,
|
createdBy: $createdBy,
|
||||||
metadata: $metadata,
|
metadata: $metadata,
|
||||||
assignments: ! empty($assignments) ? $assignments : null,
|
assignments: $assignments,
|
||||||
scopeTags: ! empty($scopeTags) ? $scopeTags : null,
|
scopeTags: $scopeTags,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Filament\Resources\BackupSetResource;
|
use App\Filament\Resources\BackupSetResource;
|
||||||
use App\Models\Policy;
|
use App\Models\Policy;
|
||||||
|
use App\Models\PolicyVersion;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
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,
|
'pageClass' => \App\Filament\Resources\BackupSetResource\Pages\ViewBackupSet::class,
|
||||||
])->callTableAction('addPolicies', data: [
|
])->callTableAction('addPolicies', data: [
|
||||||
'policy_ids' => [$policyA->id, $policyB->id],
|
'policy_ids' => [$policyA->id, $policyB->id],
|
||||||
|
'include_assignments' => false,
|
||||||
|
'include_scope_tags' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$backupSet->refresh();
|
$backupSet->refresh();
|
||||||
@ -117,6 +120,14 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($backupSet->items)->toHaveCount(2);
|
expect($backupSet->items)->toHaveCount(2);
|
||||||
expect($backupSet->items->first()->payload['id'])->toBe('policy-1');
|
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', [
|
$this->assertDatabaseHas('audit_logs', [
|
||||||
'action' => 'backup.created',
|
'action' => 'backup.created',
|
||||||
'resource_type' => 'backup_set',
|
'resource_type' => 'backup_set',
|
||||||
|
|||||||
55
tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php
Normal file
55
tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php
Normal 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'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user