Some checks failed
Main Confidence / confidence (push) Failing after 53s
## Summary This PR delivers three related improvements: ### 1. Finding Ownership Semantics (Spec 219) - Add responsibility/accountability labels to findings and finding exceptions - `owner_user_id` = accountable party (governance owner) - `assignee_user_id` = responsible party (technical implementer) - Expose Assign/Reassign actions in FindingResource with audit logging - Add ownership columns and filters to finding list - Propagate owner from finding to exception on creation - Tests: ownership semantics, assignment audit, workflow actions ### 2. Constitution v2.7.0 — LEAN-001 Pre-Production Lean Doctrine - New principle forbidding legacy aliases, migration shims, dual-write logic, and compatibility fixtures in a pre-production codebase - AI-agent 4-question verification gate before adding any compatibility path - Review rule: compatibility shims without answering the gate questions = merge blocker - Exit condition: LEAN-001 expires at first production deployment - Spec template: added default "Compatibility posture" block - Agent instructions: added "Pre-production compatibility check" section ### 3. Backup Set Operation Type Unification - Unified `backup_set.add_policies` and `backup_set.remove_policies` into single canonical `backup_set.update` - Removed all legacy aliases, constants, and test fixtures - Added lifecycle coverage for `backup_set.update` in config - Updated all 14+ test files referencing legacy types ### Spec Artifacts - `specs/219-finding-ownership-semantics/` — full spec, plan, tasks, research, data model, contracts, checklist ### Tests - All affected tests pass (OperationCatalog, backup set, finding workflow, ownership semantics) Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #256
213 lines
7.1 KiB
PHP
213 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Jobs\Middleware\TrackOperationRun;
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSet;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\OpsUx\RunFailureSanitizer;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Throwable;
|
|
|
|
class RemovePoliciesFromBackupSetJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public int $timeout = 240;
|
|
|
|
public bool $failOnTimeout = true;
|
|
|
|
public ?OperationRun $operationRun = null;
|
|
|
|
/**
|
|
* @param array<int,int> $backupItemIds
|
|
*/
|
|
public function __construct(
|
|
public int $backupSetId,
|
|
public array $backupItemIds,
|
|
public int $initiatorUserId,
|
|
?OperationRun $operationRun = null,
|
|
) {
|
|
$this->operationRun = $operationRun;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, object>
|
|
*/
|
|
public function middleware(): array
|
|
{
|
|
return [new TrackOperationRun];
|
|
}
|
|
|
|
public function handle(
|
|
AuditLogger $auditLogger,
|
|
): void {
|
|
$backupSet = BackupSet::query()->with(['tenant'])->find($this->backupSetId);
|
|
|
|
if (! $backupSet instanceof BackupSet) {
|
|
if ($this->operationRun) {
|
|
/** @var OperationRunService $opService */
|
|
$opService = app(OperationRunService::class);
|
|
$opService->updateRun(
|
|
$this->operationRun,
|
|
'completed',
|
|
'failed',
|
|
['failed' => 1],
|
|
[['code' => 'backup_set.not_found', 'message' => 'Backup set not found.']]
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$tenant = $backupSet->tenant;
|
|
|
|
$initiator = User::query()->find($this->initiatorUserId);
|
|
|
|
$requestedIds = collect($this->backupItemIds)
|
|
->map(fn (mixed $value): int => (int) $value)
|
|
->filter(fn (int $value): bool => $value > 0)
|
|
->unique()
|
|
->sort()
|
|
->values()
|
|
->all();
|
|
|
|
$requestedCount = count($requestedIds);
|
|
|
|
$failures = [];
|
|
|
|
try {
|
|
/** @var \Illuminate\Database\Eloquent\Collection<int, BackupItem> $items */
|
|
$items = BackupItem::query()
|
|
->where('backup_set_id', $backupSet->getKey())
|
|
->whereIn('id', $requestedIds)
|
|
->get();
|
|
|
|
$foundIds = $items->pluck('id')->map(fn (mixed $value): int => (int) $value)->all();
|
|
$missingIds = array_values(array_diff($requestedIds, $foundIds));
|
|
|
|
foreach ($missingIds as $missingId) {
|
|
$failures[] = [
|
|
'code' => 'backup_item.not_found',
|
|
'message' => RunFailureSanitizer::sanitizeMessage("Backup item {$missingId} not found (already removed?)."),
|
|
];
|
|
}
|
|
|
|
$removed = 0;
|
|
$policyIds = [];
|
|
$policyIdentifiers = [];
|
|
|
|
foreach ($items as $item) {
|
|
$item->delete();
|
|
$removed++;
|
|
|
|
if ($item->policy_id) {
|
|
$policyIds[] = (int) $item->policy_id;
|
|
}
|
|
|
|
if ($item->policy_identifier) {
|
|
$policyIdentifiers[] = (string) $item->policy_identifier;
|
|
}
|
|
}
|
|
|
|
$backupSet->update([
|
|
'item_count' => $backupSet->items()->count(),
|
|
]);
|
|
|
|
if ($tenant instanceof Tenant) {
|
|
$auditLogger->log(
|
|
tenant: $tenant,
|
|
action: 'backup.items_removed',
|
|
resourceType: 'backup_set',
|
|
resourceId: (string) $backupSet->getKey(),
|
|
status: 'success',
|
|
context: [
|
|
'metadata' => [
|
|
'removed_count' => $removed,
|
|
'requested_count' => $requestedCount,
|
|
'missing_count' => count($missingIds),
|
|
'policy_ids' => array_values(array_unique($policyIds)),
|
|
'policy_identifiers' => array_values(array_unique($policyIdentifiers)),
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
'initiator_user_id' => $initiator?->getKey(),
|
|
],
|
|
],
|
|
actorId: $initiator?->getKey(),
|
|
);
|
|
}
|
|
|
|
if ($this->operationRun) {
|
|
/** @var OperationRunService $opService */
|
|
$opService = app(OperationRunService::class);
|
|
|
|
$this->operationRun->update([
|
|
'context' => array_merge($this->operationRun->context ?? [], [
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
'requested_count' => $requestedCount,
|
|
'removed_count' => $removed,
|
|
'missing_count' => count($missingIds),
|
|
'remaining_count' => (int) $backupSet->item_count,
|
|
]),
|
|
]);
|
|
|
|
$outcome = 'succeeded';
|
|
if ($removed === 0) {
|
|
$outcome = 'failed';
|
|
} elseif ($failures !== []) {
|
|
$outcome = 'partially_succeeded';
|
|
}
|
|
|
|
$opService->updateRun(
|
|
$this->operationRun,
|
|
'completed',
|
|
$outcome,
|
|
[
|
|
'total' => $requestedCount,
|
|
'processed' => $requestedCount,
|
|
'succeeded' => $removed,
|
|
'failed' => count($missingIds),
|
|
'deleted' => $removed,
|
|
'items' => $requestedCount,
|
|
],
|
|
$failures,
|
|
);
|
|
}
|
|
|
|
} catch (Throwable $throwable) {
|
|
if ($tenant instanceof Tenant) {
|
|
$auditLogger->log(
|
|
tenant: $tenant,
|
|
action: 'backup.items_removed',
|
|
resourceType: 'backup_set',
|
|
resourceId: (string) $backupSet->getKey(),
|
|
status: 'failed',
|
|
context: [
|
|
'metadata' => [
|
|
'requested_count' => $requestedCount,
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
],
|
|
],
|
|
actorId: $initiator?->getKey(),
|
|
);
|
|
}
|
|
|
|
if ($this->operationRun) {
|
|
/** @var OperationRunService $opService */
|
|
$opService = app(OperationRunService::class);
|
|
$opService->failRun($this->operationRun, $throwable);
|
|
}
|
|
|
|
throw $throwable;
|
|
}
|
|
}
|
|
}
|