960 lines
38 KiB
PHP
960 lines
38 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Navigation;
|
|
|
|
use App\Filament\Resources\AlertDestinationResource;
|
|
use App\Filament\Resources\AlertRuleResource;
|
|
use App\Filament\Resources\BackupSetResource;
|
|
use App\Filament\Resources\BaselineProfileResource;
|
|
use App\Filament\Resources\BaselineSnapshotResource;
|
|
use App\Filament\Resources\FindingExceptionResource;
|
|
use App\Filament\Resources\FindingResource;
|
|
use App\Filament\Resources\PolicyVersionResource;
|
|
use App\Filament\Resources\RestoreRunResource;
|
|
use App\Models\AuditLog;
|
|
use App\Models\BackupSet;
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\BaselineSnapshotItem;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\RestoreRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Services\Auth\CapabilityResolver;
|
|
use App\Services\Auth\WorkspaceCapabilityResolver;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\References\ReferenceClass;
|
|
use App\Support\References\ReferenceDescriptor;
|
|
use App\Support\References\ReferenceResolverRegistry;
|
|
use App\Support\References\RelatedContextReferenceAdapter;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Arr;
|
|
|
|
final class RelatedNavigationResolver
|
|
{
|
|
public function __construct(
|
|
private readonly CrossResourceNavigationMatrix $matrix,
|
|
private readonly RelatedActionLabelCatalog $labels,
|
|
private readonly CapabilityResolver $capabilityResolver,
|
|
private readonly WorkspaceCapabilityResolver $workspaceCapabilityResolver,
|
|
private readonly ReferenceResolverRegistry $referenceResolverRegistry,
|
|
private readonly RelatedContextReferenceAdapter $relatedContextReferenceAdapter,
|
|
) {}
|
|
|
|
/**
|
|
* @return list<array{
|
|
* key: string,
|
|
* label: string,
|
|
* value: string,
|
|
* secondaryValue: ?string,
|
|
* targetUrl: ?string,
|
|
* targetKind: string,
|
|
* availability: string,
|
|
* unavailableReason: ?string,
|
|
* contextBadge: ?string,
|
|
* priority: int,
|
|
* actionLabel: string
|
|
* }>
|
|
*/
|
|
public function detailEntries(string $sourceType, Model $record): array
|
|
{
|
|
return array_map(
|
|
static fn (RelatedContextEntry $entry): array => $entry->toArray(),
|
|
$this->resolveEntries($sourceType, CrossResourceNavigationMatrix::SURFACE_DETAIL_SECTION, $record),
|
|
);
|
|
}
|
|
|
|
public function primaryListAction(string $sourceType, Model $record): ?RelatedContextEntry
|
|
{
|
|
$entries = array_values(array_filter(
|
|
$this->resolveEntries($sourceType, CrossResourceNavigationMatrix::SURFACE_LIST_ROW, $record),
|
|
static fn (RelatedContextEntry $entry): bool => $entry->isAvailable(),
|
|
));
|
|
|
|
return $entries[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public function operationLinks(OperationRun $run, ?Tenant $tenant): array
|
|
{
|
|
$entries = array_filter(
|
|
$this->resolveEntries(CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN, CrossResourceNavigationMatrix::SURFACE_DETAIL_SECTION, $run),
|
|
static fn (RelatedContextEntry $entry): bool => $entry->isAvailable(),
|
|
);
|
|
|
|
$links = [];
|
|
|
|
foreach ($entries as $entry) {
|
|
$links[$entry->actionLabel] = (string) $entry->targetUrl;
|
|
}
|
|
|
|
if ($tenant instanceof Tenant) {
|
|
$links = ['Open operations' => OperationRunLinks::index($tenant)] + $links;
|
|
} else {
|
|
$links = ['Open operations' => OperationRunLinks::index()] + $links;
|
|
}
|
|
|
|
return $links;
|
|
}
|
|
|
|
/**
|
|
* @return list<RelatedContextEntry>
|
|
*/
|
|
public function headerEntries(string $sourceType, Model $record): array
|
|
{
|
|
return $this->resolveEntries($sourceType, CrossResourceNavigationMatrix::SURFACE_DETAIL_HEADER, $record);
|
|
}
|
|
|
|
/**
|
|
* @return array{label: string, url: string}|null
|
|
*/
|
|
public function auditTargetLink(AuditLog $record): ?array
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$resourceType = is_string($record->resource_type) ? $record->resource_type : null;
|
|
$resourceId = is_numeric($record->resource_id) ? (int) $record->resource_id : null;
|
|
|
|
if ($resourceType === null || $resourceId === null) {
|
|
return null;
|
|
}
|
|
|
|
$tenant = $record->tenant;
|
|
$workspace = $record->workspace;
|
|
|
|
return match ($resourceType) {
|
|
'operation_run' => $workspace instanceof Workspace
|
|
&& $this->workspaceCapabilityResolver->isMember($user, $workspace)
|
|
&& OperationRun::query()
|
|
->whereKey($resourceId)
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->exists()
|
|
? ['label' => 'Open operation run', 'url' => route('admin.operations.view', ['run' => $resourceId])]
|
|
: null,
|
|
'baseline_profile' => $workspace instanceof Workspace
|
|
&& $this->workspaceCapabilityResolver->isMember($user, $workspace)
|
|
&& $this->workspaceCapabilityResolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_VIEW)
|
|
&& ($baselineProfile = BaselineProfile::query()
|
|
->whereKey($resourceId)
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->first()) instanceof BaselineProfile
|
|
&& BaselineProfileResource::canView($baselineProfile)
|
|
? ['label' => 'Open baseline profile', 'url' => BaselineProfileResource::getUrl('view', ['record' => $resourceId], panel: 'admin')]
|
|
: null,
|
|
'baseline_snapshot' => $workspace instanceof Workspace
|
|
&& $this->workspaceCapabilityResolver->isMember($user, $workspace)
|
|
&& $this->workspaceCapabilityResolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_VIEW)
|
|
&& ($baselineSnapshot = BaselineSnapshot::query()
|
|
->whereKey($resourceId)
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->first()) instanceof BaselineSnapshot
|
|
&& BaselineSnapshotResource::canView($baselineSnapshot)
|
|
? ['label' => 'Open baseline snapshot', 'url' => BaselineSnapshotResource::getUrl('view', ['record' => $resourceId], panel: 'admin')]
|
|
: null,
|
|
'alert_rule' => $workspace instanceof Workspace
|
|
&& $this->workspaceCapabilityResolver->isMember($user, $workspace)
|
|
&& ($alertRule = AlertRule::query()
|
|
->whereKey($resourceId)
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->first()) instanceof AlertRule
|
|
&& AlertRuleResource::canView($alertRule)
|
|
? ['label' => 'Open alert rule', 'url' => AlertRuleResource::getUrl('view', ['record' => $resourceId], panel: 'admin')]
|
|
: null,
|
|
'alert_destination' => $workspace instanceof Workspace
|
|
&& $this->workspaceCapabilityResolver->isMember($user, $workspace)
|
|
&& ($alertDestination = AlertDestination::query()
|
|
->whereKey($resourceId)
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->first()) instanceof AlertDestination
|
|
&& AlertDestinationResource::canView($alertDestination)
|
|
? ['label' => 'Open alert destination', 'url' => AlertDestinationResource::getUrl('view', ['record' => $resourceId], panel: 'admin')]
|
|
: null,
|
|
'backup_set' => $tenant instanceof Tenant
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_VIEW)
|
|
&& BackupSet::query()
|
|
->whereKey($resourceId)
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->exists()
|
|
? ['label' => 'Open backup set', 'url' => BackupSetResource::getUrl('view', ['record' => $resourceId], panel: 'tenant', tenant: $tenant)]
|
|
: null,
|
|
'restore_run' => $tenant instanceof Tenant
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_VIEW)
|
|
&& RestoreRun::query()
|
|
->whereKey($resourceId)
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->exists()
|
|
? ['label' => 'Open restore run', 'url' => RestoreRunResource::getUrl('view', ['record' => $resourceId], panel: 'tenant', tenant: $tenant)]
|
|
: null,
|
|
'finding' => $tenant instanceof Tenant
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_FINDINGS_VIEW)
|
|
&& Finding::query()
|
|
->whereKey($resourceId)
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->exists()
|
|
? ['label' => 'Open finding', 'url' => FindingResource::getUrl('view', ['record' => $resourceId], panel: 'tenant', tenant: $tenant)]
|
|
: null,
|
|
'finding_exception' => $tenant instanceof Tenant
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::FINDING_EXCEPTION_VIEW)
|
|
&& ($findingException = FindingException::query()
|
|
->whereKey($resourceId)
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->first()) instanceof FindingException
|
|
? ['label' => 'Open finding exception', 'url' => FindingExceptionResource::getUrl('view', ['record' => $findingException], panel: 'tenant', tenant: $tenant)]
|
|
: null,
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return list<RelatedContextEntry>
|
|
*/
|
|
private function resolveEntries(string $sourceType, string $surface, Model $record): array
|
|
{
|
|
$entries = [];
|
|
|
|
foreach ($this->matrix->rulesFor($sourceType, $surface) as $rule) {
|
|
$entry = $this->resolveRule($rule, $record);
|
|
|
|
if ($entry instanceof RelatedContextEntry) {
|
|
$entries[] = $entry;
|
|
}
|
|
}
|
|
|
|
usort(
|
|
$entries,
|
|
static fn (RelatedContextEntry $left, RelatedContextEntry $right): int => $left->priority <=> $right->priority,
|
|
);
|
|
|
|
return $entries;
|
|
}
|
|
|
|
private function resolveRule(NavigationMatrixRule $rule, Model $record): ?RelatedContextEntry
|
|
{
|
|
return match ($rule->sourceType) {
|
|
CrossResourceNavigationMatrix::SOURCE_FINDING => $record instanceof Finding ? $this->resolveFindingRule($rule, $record) : null,
|
|
CrossResourceNavigationMatrix::SOURCE_POLICY_VERSION => $record instanceof PolicyVersion ? $this->resolvePolicyVersionRule($rule, $record) : null,
|
|
CrossResourceNavigationMatrix::SOURCE_BASELINE_SNAPSHOT => $record instanceof BaselineSnapshot ? $this->resolveBaselineSnapshotRule($rule, $record) : null,
|
|
CrossResourceNavigationMatrix::SOURCE_BACKUP_SET => $record instanceof BackupSet ? $this->resolveBackupSetRule($rule, $record) : null,
|
|
CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN => $record instanceof OperationRun ? $this->resolveOperationRunRule($rule, $record) : null,
|
|
CrossResourceNavigationMatrix::SOURCE_BASELINE_PROFILE => $record instanceof BaselineProfile ? $this->resolveBaselineProfileRule($rule, $record) : null,
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function resolveFindingRule(NavigationMatrixRule $rule, Finding $finding): ?RelatedContextEntry
|
|
{
|
|
return match ($rule->relationKey) {
|
|
'baseline_snapshot' => $this->baselineSnapshotEntry(
|
|
rule: $rule,
|
|
snapshotId: $this->findingSnapshotId($finding),
|
|
workspaceId: (int) $finding->workspace_id,
|
|
),
|
|
'source_run' => $this->operationRunEntry(
|
|
rule: $rule,
|
|
runId: $this->findingRunId($finding),
|
|
workspaceId: (int) $finding->workspace_id,
|
|
context: $this->contextForFinding($finding, $rule->sourceSurface),
|
|
),
|
|
'current_policy_version' => $this->policyVersionEntry(
|
|
rule: $rule,
|
|
policyVersionId: $this->findingPolicyVersionId($finding),
|
|
tenantId: (int) $finding->tenant_id,
|
|
),
|
|
'parent_policy' => $this->parentPolicyEntryForFinding($rule, $finding),
|
|
'baseline_profile' => $this->baselineProfileEntry(
|
|
rule: $rule,
|
|
profileId: $this->findingProfileId($finding),
|
|
workspaceId: (int) $finding->workspace_id,
|
|
),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function resolvePolicyVersionRule(NavigationMatrixRule $rule, PolicyVersion $version): ?RelatedContextEntry
|
|
{
|
|
return match ($rule->relationKey) {
|
|
'parent_policy' => $this->policyEntry(
|
|
rule: $rule,
|
|
policy: $version->policy,
|
|
),
|
|
'baseline_snapshot' => $this->policyVersionSnapshotEntry($rule, $version),
|
|
'baseline_profile' => $this->baselineProfileEntry(
|
|
rule: $rule,
|
|
profileId: is_numeric($version->baseline_profile_id ?? null) ? (int) $version->baseline_profile_id : null,
|
|
workspaceId: (int) $version->workspace_id,
|
|
),
|
|
'source_run' => $this->operationRunEntry(
|
|
rule: $rule,
|
|
runId: is_numeric($version->operation_run_id ?? null) ? (int) $version->operation_run_id : null,
|
|
workspaceId: (int) $version->workspace_id,
|
|
context: $this->contextForPolicyVersion($version, $rule->sourceSurface),
|
|
),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function resolveBaselineSnapshotRule(NavigationMatrixRule $rule, BaselineSnapshot $snapshot): ?RelatedContextEntry
|
|
{
|
|
return match ($rule->relationKey) {
|
|
'baseline_profile' => $this->policyProfileEntry(
|
|
rule: $rule,
|
|
profile: $snapshot->baselineProfile,
|
|
),
|
|
'source_run' => $this->snapshotRunEntry($rule, $snapshot),
|
|
'policy_version' => $this->snapshotPolicyVersionEntry($rule, $snapshot),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function resolveBackupSetRule(NavigationMatrixRule $rule, BackupSet $backupSet): ?RelatedContextEntry
|
|
{
|
|
return match ($rule->relationKey) {
|
|
'source_run' => $this->backupSetRunEntry($rule, $backupSet),
|
|
'operations' => $this->operationsEntry(
|
|
rule: $rule,
|
|
tenant: $backupSet->tenant,
|
|
context: $this->contextForBackupSet($backupSet, $rule->sourceSurface),
|
|
),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function resolveOperationRunRule(NavigationMatrixRule $rule, OperationRun $run): ?RelatedContextEntry
|
|
{
|
|
$context = is_array($run->context) ? $run->context : [];
|
|
|
|
return match ($rule->relationKey) {
|
|
'backup_set' => $this->backupSetEntry(
|
|
rule: $rule,
|
|
backupSetId: is_numeric($context['backup_set_id'] ?? null) ? (int) $context['backup_set_id'] : null,
|
|
tenantId: is_numeric($run->tenant_id ?? null) ? (int) $run->tenant_id : null,
|
|
),
|
|
'restore_run' => $this->restoreRunEntry(
|
|
rule: $rule,
|
|
restoreRunId: is_numeric($context['restore_run_id'] ?? null) ? (int) $context['restore_run_id'] : null,
|
|
tenantId: is_numeric($run->tenant_id ?? null) ? (int) $run->tenant_id : null,
|
|
),
|
|
'baseline_profile' => $this->baselineProfileEntry(
|
|
rule: $rule,
|
|
profileId: is_numeric($context['baseline_profile_id'] ?? null) ? (int) $context['baseline_profile_id'] : null,
|
|
workspaceId: (int) $run->workspace_id,
|
|
),
|
|
'baseline_snapshot' => $this->baselineSnapshotEntry(
|
|
rule: $rule,
|
|
snapshotId: is_numeric($context['baseline_snapshot_id'] ?? null) ? (int) $context['baseline_snapshot_id'] : null,
|
|
workspaceId: (int) $run->workspace_id,
|
|
),
|
|
'parent_policy' => $this->operationRunPolicyEntry($rule, $run),
|
|
'operations' => $this->operationsEntry(
|
|
rule: $rule,
|
|
tenant: $run->tenant,
|
|
context: $this->contextForOperationRun($run),
|
|
),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function resolveBaselineProfileRule(NavigationMatrixRule $rule, BaselineProfile $profile): ?RelatedContextEntry
|
|
{
|
|
return match ($rule->relationKey) {
|
|
'baseline_snapshot' => $this->baselineSnapshotEntry(
|
|
rule: $rule,
|
|
snapshotId: is_numeric($profile->active_snapshot_id ?? null) ? (int) $profile->active_snapshot_id : null,
|
|
workspaceId: (int) $profile->workspace_id,
|
|
),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function findingSnapshotId(Finding $finding): ?int
|
|
{
|
|
$snapshotId = Arr::get($finding->evidence_jsonb ?? [], 'provenance.baseline_snapshot_id');
|
|
|
|
if (! is_numeric($snapshotId)) {
|
|
$snapshotId = Arr::get($finding->evidence_jsonb ?? [], 'baseline_snapshot_id');
|
|
}
|
|
|
|
return is_numeric($snapshotId) ? (int) $snapshotId : null;
|
|
}
|
|
|
|
private function findingProfileId(Finding $finding): ?int
|
|
{
|
|
$profileId = Arr::get($finding->evidence_jsonb ?? [], 'provenance.baseline_profile_id');
|
|
|
|
return is_numeric($profileId) ? (int) $profileId : null;
|
|
}
|
|
|
|
private function findingPolicyVersionId(Finding $finding): ?int
|
|
{
|
|
$policyVersionId = Arr::get($finding->evidence_jsonb ?? [], 'current.policy_version_id');
|
|
|
|
if (! is_numeric($policyVersionId)) {
|
|
$policyVersionId = Arr::get($finding->evidence_jsonb ?? [], 'baseline.policy_version_id');
|
|
}
|
|
|
|
return is_numeric($policyVersionId) ? (int) $policyVersionId : null;
|
|
}
|
|
|
|
private function findingRunId(Finding $finding): ?int
|
|
{
|
|
$runId = Arr::get($finding->evidence_jsonb ?? [], 'provenance.compare_operation_run_id');
|
|
|
|
if (is_numeric($runId)) {
|
|
return (int) $runId;
|
|
}
|
|
|
|
if (is_numeric($finding->current_operation_run_id ?? null)) {
|
|
return (int) $finding->current_operation_run_id;
|
|
}
|
|
|
|
return is_numeric($finding->baseline_operation_run_id ?? null)
|
|
? (int) $finding->baseline_operation_run_id
|
|
: null;
|
|
}
|
|
|
|
private function policyVersionSnapshotEntry(NavigationMatrixRule $rule, PolicyVersion $version): ?RelatedContextEntry
|
|
{
|
|
$snapshotItem = BaselineSnapshotItem::query()
|
|
->where('meta_jsonb->version_reference->policy_version_id', (int) $version->getKey())
|
|
->whereHas('snapshot', fn ($query) => $query->where('workspace_id', (int) $version->workspace_id))
|
|
->with('snapshot.baselineProfile')
|
|
->latest('id')
|
|
->first();
|
|
|
|
if (! $snapshotItem instanceof BaselineSnapshotItem) {
|
|
return null;
|
|
}
|
|
|
|
$snapshot = $snapshotItem->snapshot;
|
|
|
|
return $snapshot instanceof BaselineSnapshot
|
|
? $this->baselineSnapshotEntry($rule, (int) $snapshot->getKey(), (int) $version->workspace_id)
|
|
: null;
|
|
}
|
|
|
|
private function snapshotRunEntry(NavigationMatrixRule $rule, BaselineSnapshot $snapshot): ?RelatedContextEntry
|
|
{
|
|
$candidate = OperationRun::query()
|
|
->where('workspace_id', (int) $snapshot->workspace_id)
|
|
->where('context->baseline_snapshot_id', (int) $snapshot->getKey())
|
|
->orderByDesc('completed_at')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
return $candidate instanceof OperationRun
|
|
? $this->operationRunEntry(
|
|
rule: $rule,
|
|
runId: (int) $candidate->getKey(),
|
|
workspaceId: (int) $snapshot->workspace_id,
|
|
context: $this->contextForBaselineSnapshot($snapshot, $rule->sourceSurface),
|
|
)
|
|
: null;
|
|
}
|
|
|
|
private function snapshotPolicyVersionEntry(NavigationMatrixRule $rule, BaselineSnapshot $snapshot): ?RelatedContextEntry
|
|
{
|
|
$snapshotItem = BaselineSnapshotItem::query()
|
|
->where('baseline_snapshot_id', (int) $snapshot->getKey())
|
|
->whereNotNull('meta_jsonb->version_reference->policy_version_id')
|
|
->orderBy('id')
|
|
->first();
|
|
|
|
if (! $snapshotItem instanceof BaselineSnapshotItem) {
|
|
return null;
|
|
}
|
|
|
|
$policyVersionId = data_get($snapshotItem->meta_jsonb, 'version_reference.policy_version_id');
|
|
|
|
return $this->policyVersionEntry(
|
|
rule: $rule,
|
|
policyVersionId: is_numeric($policyVersionId) ? (int) $policyVersionId : null,
|
|
tenantId: $this->activeTenantId(),
|
|
);
|
|
}
|
|
|
|
private function backupSetRunEntry(NavigationMatrixRule $rule, BackupSet $backupSet): ?RelatedContextEntry
|
|
{
|
|
$candidate = OperationRun::query()
|
|
->where('tenant_id', (int) $backupSet->tenant_id)
|
|
->where('context->backup_set_id', (int) $backupSet->getKey())
|
|
->orderByDesc('completed_at')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
return $candidate instanceof OperationRun
|
|
? $this->operationRunEntry(
|
|
rule: $rule,
|
|
runId: (int) $candidate->getKey(),
|
|
workspaceId: (int) $backupSet->workspace_id,
|
|
context: $this->contextForBackupSet($backupSet, $rule->sourceSurface),
|
|
)
|
|
: null;
|
|
}
|
|
|
|
private function operationRunPolicyEntry(NavigationMatrixRule $rule, OperationRun $run): ?RelatedContextEntry
|
|
{
|
|
$policyId = data_get($run->context, 'policy_id');
|
|
|
|
if (! is_numeric($policyId)) {
|
|
return null;
|
|
}
|
|
|
|
$policy = Policy::query()
|
|
->whereKey((int) $policyId)
|
|
->where('tenant_id', (int) $run->tenant_id)
|
|
->first();
|
|
|
|
return $this->policyEntry($rule, $policy);
|
|
}
|
|
|
|
private function parentPolicyEntryForFinding(NavigationMatrixRule $rule, Finding $finding): ?RelatedContextEntry
|
|
{
|
|
$policyVersionId = $this->findingPolicyVersionId($finding);
|
|
|
|
if ($policyVersionId === null) {
|
|
return null;
|
|
}
|
|
|
|
$version = PolicyVersion::query()
|
|
->with('policy')
|
|
->whereKey($policyVersionId)
|
|
->where('tenant_id', (int) $finding->tenant_id)
|
|
->first();
|
|
|
|
if (! $version instanceof PolicyVersion) {
|
|
return null;
|
|
}
|
|
|
|
return $this->policyEntry($rule, $version->policy);
|
|
}
|
|
|
|
private function baselineSnapshotEntry(
|
|
NavigationMatrixRule $rule,
|
|
?int $snapshotId,
|
|
int $workspaceId,
|
|
): ?RelatedContextEntry {
|
|
if ($snapshotId === null || $snapshotId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return $this->resolveReferenceEntry(
|
|
rule: $rule,
|
|
descriptor: new ReferenceDescriptor(
|
|
referenceClass: ReferenceClass::BaselineSnapshot,
|
|
rawIdentifier: (string) $snapshotId,
|
|
workspaceId: $workspaceId,
|
|
sourceType: $rule->sourceType,
|
|
sourceSurface: $rule->sourceSurface,
|
|
linkedModelId: $snapshotId,
|
|
),
|
|
);
|
|
}
|
|
|
|
private function baselineProfileEntry(
|
|
NavigationMatrixRule $rule,
|
|
?int $profileId,
|
|
int $workspaceId,
|
|
): ?RelatedContextEntry {
|
|
if ($profileId === null || $profileId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$profile = BaselineProfile::query()
|
|
->whereKey($profileId)
|
|
->where('workspace_id', $workspaceId)
|
|
->first();
|
|
|
|
if (! $profile instanceof BaselineProfile) {
|
|
return $this->unavailableEntry($rule, (string) $profileId, 'missing');
|
|
}
|
|
|
|
return $this->policyProfileEntry($rule, $profile);
|
|
}
|
|
|
|
private function policyProfileEntry(NavigationMatrixRule $rule, ?BaselineProfile $profile): ?RelatedContextEntry
|
|
{
|
|
if (! $profile instanceof BaselineProfile) {
|
|
return null;
|
|
}
|
|
|
|
return $this->resolveReferenceEntry(
|
|
rule: $rule,
|
|
descriptor: new ReferenceDescriptor(
|
|
referenceClass: ReferenceClass::BaselineProfile,
|
|
rawIdentifier: (string) $profile->getKey(),
|
|
workspaceId: (int) $profile->workspace_id,
|
|
sourceType: $rule->sourceType,
|
|
sourceSurface: $rule->sourceSurface,
|
|
fallbackLabel: (string) $profile->name,
|
|
linkedModelId: (int) $profile->getKey(),
|
|
),
|
|
);
|
|
}
|
|
|
|
private function operationRunEntry(
|
|
NavigationMatrixRule $rule,
|
|
?int $runId,
|
|
int $workspaceId,
|
|
?CanonicalNavigationContext $context = null,
|
|
): ?RelatedContextEntry {
|
|
if ($runId === null || $runId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return $this->resolveReferenceEntry(
|
|
rule: $rule,
|
|
descriptor: new ReferenceDescriptor(
|
|
referenceClass: ReferenceClass::OperationRun,
|
|
rawIdentifier: (string) $runId,
|
|
workspaceId: $workspaceId,
|
|
sourceType: $rule->sourceType,
|
|
sourceSurface: $rule->sourceSurface,
|
|
linkedModelId: $runId,
|
|
context: [
|
|
'navigation_context' => $context,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
private function policyVersionEntry(
|
|
NavigationMatrixRule $rule,
|
|
?int $policyVersionId,
|
|
?int $tenantId,
|
|
): ?RelatedContextEntry {
|
|
if ($policyVersionId === null || $policyVersionId <= 0 || $tenantId === null || $tenantId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return $this->resolveReferenceEntry(
|
|
rule: $rule,
|
|
descriptor: new ReferenceDescriptor(
|
|
referenceClass: ReferenceClass::PolicyVersion,
|
|
rawIdentifier: (string) $policyVersionId,
|
|
tenantId: $tenantId,
|
|
sourceType: $rule->sourceType,
|
|
sourceSurface: $rule->sourceSurface,
|
|
linkedModelId: $policyVersionId,
|
|
),
|
|
);
|
|
}
|
|
|
|
private function policyEntry(NavigationMatrixRule $rule, ?Policy $policy): ?RelatedContextEntry
|
|
{
|
|
if (! $policy instanceof Policy) {
|
|
return null;
|
|
}
|
|
|
|
return $this->resolveReferenceEntry(
|
|
rule: $rule,
|
|
descriptor: new ReferenceDescriptor(
|
|
referenceClass: ReferenceClass::Policy,
|
|
rawIdentifier: (string) $policy->getKey(),
|
|
tenantId: is_numeric($policy->tenant_id ?? null) ? (int) $policy->tenant_id : null,
|
|
sourceType: $rule->sourceType,
|
|
sourceSurface: $rule->sourceSurface,
|
|
fallbackLabel: (string) ($policy->display_name ?: 'Policy'),
|
|
linkedModelId: (int) $policy->getKey(),
|
|
),
|
|
);
|
|
}
|
|
|
|
private function backupSetEntry(
|
|
NavigationMatrixRule $rule,
|
|
?int $backupSetId,
|
|
?int $tenantId,
|
|
): ?RelatedContextEntry {
|
|
if ($backupSetId === null || $backupSetId <= 0 || $tenantId === null || $tenantId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return $this->resolveReferenceEntry(
|
|
rule: $rule,
|
|
descriptor: new ReferenceDescriptor(
|
|
referenceClass: ReferenceClass::BackupSet,
|
|
rawIdentifier: (string) $backupSetId,
|
|
tenantId: $tenantId,
|
|
sourceType: $rule->sourceType,
|
|
sourceSurface: $rule->sourceSurface,
|
|
linkedModelId: $backupSetId,
|
|
),
|
|
);
|
|
}
|
|
|
|
private function resolveReferenceEntry(NavigationMatrixRule $rule, ReferenceDescriptor $descriptor): ?RelatedContextEntry
|
|
{
|
|
return $this->relatedContextReferenceAdapter->adapt(
|
|
rule: $rule,
|
|
reference: $this->referenceResolverRegistry->resolve($descriptor),
|
|
);
|
|
}
|
|
|
|
private function restoreRunEntry(
|
|
NavigationMatrixRule $rule,
|
|
?int $restoreRunId,
|
|
?int $tenantId,
|
|
): ?RelatedContextEntry {
|
|
if ($restoreRunId === null || $restoreRunId <= 0 || $tenantId === null || $tenantId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$restoreRun = RestoreRun::query()
|
|
->with('tenant')
|
|
->whereKey($restoreRunId)
|
|
->where('tenant_id', $tenantId)
|
|
->first();
|
|
|
|
if (! $restoreRun instanceof RestoreRun) {
|
|
return $this->unavailableEntry($rule, (string) $restoreRunId, 'missing');
|
|
}
|
|
|
|
if (! $this->canOpenTenantRecord($restoreRun->tenant, Capabilities::TENANT_VIEW)) {
|
|
return $this->unavailableEntry($rule, '#'.$restoreRunId, 'unauthorized');
|
|
}
|
|
|
|
return RelatedContextEntry::available(
|
|
key: $rule->relationKey,
|
|
label: $this->labels->entryLabel($rule->relationKey),
|
|
value: 'Restore run',
|
|
secondaryValue: '#'.$restoreRun->getKey(),
|
|
targetUrl: RestoreRunResource::getUrl('view', ['record' => $restoreRun], tenant: $restoreRun->tenant),
|
|
targetKind: $rule->targetType,
|
|
priority: $rule->priority,
|
|
actionLabel: $this->labels->actionLabel($rule->relationKey),
|
|
contextBadge: 'Tenant',
|
|
);
|
|
}
|
|
|
|
private function operationsEntry(
|
|
NavigationMatrixRule $rule,
|
|
?Tenant $tenant,
|
|
?CanonicalNavigationContext $context = null,
|
|
): ?RelatedContextEntry {
|
|
if (! $tenant instanceof Tenant) {
|
|
return null;
|
|
}
|
|
|
|
if (! $this->canOpenTenantRecord($tenant, Capabilities::TENANT_VIEW)) {
|
|
return null;
|
|
}
|
|
|
|
return RelatedContextEntry::available(
|
|
key: $rule->relationKey,
|
|
label: $this->labels->entryLabel($rule->relationKey),
|
|
value: 'Operations',
|
|
secondaryValue: $tenant->name,
|
|
targetUrl: OperationRunLinks::index($tenant, $context),
|
|
targetKind: $rule->targetType,
|
|
priority: $rule->priority,
|
|
actionLabel: $this->labels->actionLabel($rule->relationKey),
|
|
contextBadge: 'Tenant context',
|
|
);
|
|
}
|
|
|
|
private function unavailableEntry(NavigationMatrixRule $rule, ?string $referenceValue, string $reason): ?RelatedContextEntry
|
|
{
|
|
if ($rule->missingStatePolicy === 'hide') {
|
|
return null;
|
|
}
|
|
|
|
return RelatedContextEntry::unavailable(
|
|
key: $rule->relationKey,
|
|
label: $this->labels->entryLabel($rule->relationKey),
|
|
state: new UnavailableRelationState(
|
|
relationKey: $rule->relationKey,
|
|
referenceValue: $referenceValue,
|
|
reason: $reason,
|
|
message: $this->labels->unavailableMessage($rule->relationKey, $reason),
|
|
showReference: $rule->missingStatePolicy === 'show_reference_only',
|
|
),
|
|
targetKind: $rule->targetType,
|
|
priority: $rule->priority,
|
|
actionLabel: $this->labels->actionLabel($rule->relationKey),
|
|
);
|
|
}
|
|
|
|
private function canOpenOperationRun(OperationRun $run): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
return $user instanceof User && $user->can('view', $run);
|
|
}
|
|
|
|
private function canOpenWorkspaceBaselines(int $workspaceId): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
$workspace = Workspace::query()->whereKey($workspaceId)->first();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return false;
|
|
}
|
|
|
|
return $this->workspaceCapabilityResolver->isMember($user, $workspace)
|
|
&& $this->workspaceCapabilityResolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_VIEW);
|
|
}
|
|
|
|
private function canOpenTenantRecord(?Tenant $tenant, string $capability): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
return $tenant instanceof Tenant
|
|
&& $user instanceof User
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, $capability);
|
|
}
|
|
|
|
private function canOpenPolicyVersion(PolicyVersion $version): bool
|
|
{
|
|
$tenant = $version->tenant;
|
|
|
|
if (! $tenant instanceof Tenant || ! $this->canOpenTenantRecord($tenant, Capabilities::TENANT_VIEW)) {
|
|
return false;
|
|
}
|
|
|
|
if (in_array((string) $version->capture_purpose?->value, ['baseline_capture', 'baseline_compare'], true)) {
|
|
$user = auth()->user();
|
|
|
|
return $user instanceof User
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_SYNC);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function canOpenPolicy(Policy $policy): bool
|
|
{
|
|
return $this->canOpenTenantRecord($policy->tenant, Capabilities::TENANT_VIEW);
|
|
}
|
|
|
|
private function contextForFinding(Finding $finding, string $surface): CanonicalNavigationContext
|
|
{
|
|
$tenant = $finding->tenant;
|
|
$backLabel = $surface === CrossResourceNavigationMatrix::SURFACE_LIST_ROW ? 'Back to findings' : 'Back to finding';
|
|
$backUrl = $surface === CrossResourceNavigationMatrix::SURFACE_LIST_ROW
|
|
? FindingResource::getUrl('index', tenant: $tenant)
|
|
: FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant);
|
|
|
|
return new CanonicalNavigationContext(
|
|
sourceSurface: 'finding.'.$surface,
|
|
canonicalRouteName: 'admin.operations.view',
|
|
tenantId: $tenant?->getKey(),
|
|
backLinkLabel: $backLabel,
|
|
backLinkUrl: $backUrl,
|
|
filterPayload: $tenant instanceof Tenant ? [
|
|
'tableFilters' => [
|
|
'tenant_id' => ['value' => (string) $tenant->getKey()],
|
|
],
|
|
] : [],
|
|
);
|
|
}
|
|
|
|
private function contextForPolicyVersion(PolicyVersion $version, string $surface): CanonicalNavigationContext
|
|
{
|
|
$tenant = $version->tenant;
|
|
$backLabel = $surface === CrossResourceNavigationMatrix::SURFACE_LIST_ROW ? 'Back to policy versions' : 'Back to policy version';
|
|
$backUrl = $surface === CrossResourceNavigationMatrix::SURFACE_LIST_ROW
|
|
? PolicyVersionResource::getUrl('index', tenant: $tenant)
|
|
: PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant);
|
|
|
|
return new CanonicalNavigationContext(
|
|
sourceSurface: 'policy_version.'.$surface,
|
|
canonicalRouteName: 'admin.operations.view',
|
|
tenantId: $tenant?->getKey(),
|
|
backLinkLabel: $backLabel,
|
|
backLinkUrl: $backUrl,
|
|
filterPayload: $tenant instanceof Tenant ? [
|
|
'tableFilters' => [
|
|
'tenant_id' => ['value' => (string) $tenant->getKey()],
|
|
],
|
|
] : [],
|
|
);
|
|
}
|
|
|
|
private function contextForBaselineSnapshot(BaselineSnapshot $snapshot, string $surface): CanonicalNavigationContext
|
|
{
|
|
$backLabel = $surface === CrossResourceNavigationMatrix::SURFACE_LIST_ROW ? 'Back to baseline snapshots' : 'Back to baseline snapshot';
|
|
$backUrl = $surface === CrossResourceNavigationMatrix::SURFACE_LIST_ROW
|
|
? BaselineSnapshotResource::getUrl(panel: 'admin')
|
|
: BaselineSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin');
|
|
|
|
return new CanonicalNavigationContext(
|
|
sourceSurface: 'baseline_snapshot.'.$surface,
|
|
canonicalRouteName: 'admin.operations.view',
|
|
backLinkLabel: $backLabel,
|
|
backLinkUrl: $backUrl,
|
|
);
|
|
}
|
|
|
|
private function contextForBackupSet(BackupSet $backupSet, string $surface): CanonicalNavigationContext
|
|
{
|
|
$tenant = $backupSet->tenant;
|
|
$backLabel = $surface === CrossResourceNavigationMatrix::SURFACE_LIST_ROW ? 'Back to backup sets' : 'Back to backup set';
|
|
$backUrl = $surface === CrossResourceNavigationMatrix::SURFACE_LIST_ROW
|
|
? BackupSetResource::getUrl('index', tenant: $tenant)
|
|
: BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant);
|
|
|
|
return new CanonicalNavigationContext(
|
|
sourceSurface: 'backup_set.'.$surface,
|
|
canonicalRouteName: 'admin.operations.view',
|
|
tenantId: $tenant?->getKey(),
|
|
backLinkLabel: $backLabel,
|
|
backLinkUrl: $backUrl,
|
|
filterPayload: $tenant instanceof Tenant ? [
|
|
'tableFilters' => [
|
|
'tenant_id' => ['value' => (string) $tenant->getKey()],
|
|
],
|
|
] : [],
|
|
);
|
|
}
|
|
|
|
private function contextForOperationRun(OperationRun $run): CanonicalNavigationContext
|
|
{
|
|
$tenant = $run->tenant;
|
|
|
|
return new CanonicalNavigationContext(
|
|
sourceSurface: 'operation_run.detail_section',
|
|
canonicalRouteName: 'admin.operations.index',
|
|
tenantId: $tenant?->getKey(),
|
|
backLinkLabel: 'Back to operations',
|
|
backLinkUrl: OperationRunLinks::index($tenant),
|
|
filterPayload: $tenant instanceof Tenant ? [
|
|
'tableFilters' => [
|
|
'tenant_id' => ['value' => (string) $tenant->getKey()],
|
|
],
|
|
] : [],
|
|
);
|
|
}
|
|
|
|
private function activeTenantId(): ?int
|
|
{
|
|
$tenant = Filament::getTenant();
|
|
|
|
return $tenant instanceof Tenant ? (int) $tenant->getKey() : null;
|
|
}
|
|
}
|