Implements provider access hardening for Intune write operations: - RBAC-based write gate with configurable staleness thresholds - Gate enforced at restore start and in jobs (execute + assignments) - UI affordances: disabled rerun action, tenant RBAC status card, refresh RBAC action - Audit logging for blocked writes - Ops UX label: `rbac.health_check` now displays as “RBAC health check” - Adds/updates Pest tests and SpecKit artifacts for feature 108 Notes: - Filament v5 / Livewire v4 compliant. - Destructive actions require confirmation. - Assets: no new global assets. Tested: - `vendor/bin/sail artisan test --compact` (suite previously green) + focused OpsUx tests for OperationCatalog labels. - `vendor/bin/sail bin pint --dirty`. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #132
162 lines
4.9 KiB
PHP
162 lines
4.9 KiB
PHP
<?php
|
|
|
|
use App\Jobs\RestoreAssignmentsJob;
|
|
use App\Models\BackupSet;
|
|
use App\Models\RestoreRun;
|
|
use App\Models\Tenant;
|
|
use App\Services\AssignmentRestoreService;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\RestoreRunStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Mockery\MockInterface;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config()->set('tenantpilot.hardening.intune_write_gate.enabled', true);
|
|
config()->set('tenantpilot.hardening.intune_write_gate.freshness_threshold_hours', 24);
|
|
});
|
|
|
|
test('restore assignments job marks run failed when rbac is stale', function () {
|
|
$tenant = Tenant::factory()->create([
|
|
'rbac_status' => 'ok',
|
|
'rbac_last_checked_at' => now()->subHours(48),
|
|
]);
|
|
|
|
$backupSet = BackupSet::create([
|
|
'tenant_id' => $tenant->id,
|
|
'name' => 'Backup',
|
|
'status' => 'completed',
|
|
'item_count' => 0,
|
|
]);
|
|
|
|
$restoreRun = RestoreRun::create([
|
|
'tenant_id' => $tenant->id,
|
|
'backup_set_id' => $backupSet->id,
|
|
'requested_by' => 'actor@example.com',
|
|
'is_dry_run' => false,
|
|
'status' => RestoreRunStatus::Running->value,
|
|
'requested_items' => null,
|
|
'preview' => [],
|
|
'results' => null,
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$this->mock(AssignmentRestoreService::class, function (MockInterface $mock) {
|
|
$mock->shouldNotReceive('restore');
|
|
});
|
|
|
|
$operationRun = app(OperationRunService::class)->ensureRun(
|
|
tenant: $tenant,
|
|
type: 'assignments.restore',
|
|
inputs: [
|
|
'restore_run_id' => $restoreRun->id,
|
|
'policy_type' => 'deviceConfiguration',
|
|
'policy_id' => 'test-policy-id',
|
|
],
|
|
);
|
|
|
|
$assignments = [
|
|
['target' => ['@odata.type' => '#microsoft.graph.allLicensedUsersAssignmentTarget']],
|
|
];
|
|
|
|
$job = new RestoreAssignmentsJob(
|
|
restoreRunId: $restoreRun->id,
|
|
tenantId: (int) $tenant->getKey(),
|
|
policyType: 'deviceConfiguration',
|
|
policyId: 'test-policy-id',
|
|
assignments: $assignments,
|
|
groupMapping: [],
|
|
foundationMapping: [],
|
|
actorEmail: 'actor@example.com',
|
|
actorName: 'Actor',
|
|
operationRun: $operationRun,
|
|
);
|
|
|
|
$job->handle(
|
|
app(AssignmentRestoreService::class),
|
|
app(OperationRunService::class),
|
|
);
|
|
|
|
$operationRun->refresh();
|
|
|
|
expect($operationRun->outcome)->toBe(OperationRunOutcome::Failed->value)
|
|
->and($operationRun->status)->toBe(OperationRunStatus::Completed->value);
|
|
|
|
$failures = is_array($operationRun->failure_summary) ? $operationRun->failure_summary : [];
|
|
$reasonCodes = array_column($failures, 'reason_code');
|
|
|
|
expect($reasonCodes)->toContain('intune_rbac.stale');
|
|
});
|
|
|
|
test('restore assignments job marks run failed when rbac is not configured', function () {
|
|
$tenant = Tenant::factory()->create([
|
|
'rbac_status' => null,
|
|
'rbac_last_checked_at' => null,
|
|
]);
|
|
|
|
$backupSet = BackupSet::create([
|
|
'tenant_id' => $tenant->id,
|
|
'name' => 'Backup',
|
|
'status' => 'completed',
|
|
'item_count' => 0,
|
|
]);
|
|
|
|
$restoreRun = RestoreRun::create([
|
|
'tenant_id' => $tenant->id,
|
|
'backup_set_id' => $backupSet->id,
|
|
'requested_by' => 'actor@example.com',
|
|
'is_dry_run' => false,
|
|
'status' => RestoreRunStatus::Running->value,
|
|
'requested_items' => null,
|
|
'preview' => [],
|
|
'results' => null,
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$this->mock(AssignmentRestoreService::class, function (MockInterface $mock) {
|
|
$mock->shouldNotReceive('restore');
|
|
});
|
|
|
|
$operationRun = app(OperationRunService::class)->ensureRun(
|
|
tenant: $tenant,
|
|
type: 'assignments.restore',
|
|
inputs: [
|
|
'restore_run_id' => $restoreRun->id,
|
|
'policy_type' => 'deviceConfiguration',
|
|
'policy_id' => 'another-policy-id',
|
|
],
|
|
);
|
|
|
|
$assignments = [
|
|
['target' => ['@odata.type' => '#microsoft.graph.allLicensedUsersAssignmentTarget']],
|
|
];
|
|
|
|
$job = new RestoreAssignmentsJob(
|
|
restoreRunId: $restoreRun->id,
|
|
tenantId: (int) $tenant->getKey(),
|
|
policyType: 'deviceConfiguration',
|
|
policyId: 'another-policy-id',
|
|
assignments: $assignments,
|
|
groupMapping: [],
|
|
foundationMapping: [],
|
|
actorEmail: 'actor@example.com',
|
|
actorName: 'Actor',
|
|
operationRun: $operationRun,
|
|
);
|
|
|
|
$job->handle(
|
|
app(AssignmentRestoreService::class),
|
|
app(OperationRunService::class),
|
|
);
|
|
|
|
$operationRun->refresh();
|
|
|
|
$failures = is_array($operationRun->failure_summary) ? $operationRun->failure_summary : [];
|
|
$reasonCodes = array_column($failures, 'reason_code');
|
|
|
|
expect($reasonCodes)->toContain('intune_rbac.not_configured');
|
|
});
|