Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m12s
Added BaselineSubjectResolution page and supporting logic to visualize missing identities, ambiguous matches, and skipped coverages per Spec 384.
1239 lines
49 KiB
PHP
1239 lines
49 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\EnvironmentReviewResource;
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\FindingExceptionResource;
|
|
use App\Filament\Resources\FindingResource;
|
|
use App\Filament\Resources\PolicyVersionResource;
|
|
use App\Filament\Resources\RestoreRunResource;
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Models\AuditLog;
|
|
use App\Models\BackupSet;
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\BaselineSnapshotItem;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\RestoreRun;
|
|
use App\Models\ReviewPack;
|
|
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\Services\Baselines\BaselineSubjectResolutionQuery;
|
|
use App\Support\ManagedEnvironmentLinks;
|
|
use App\Support\OperateHub\OperateHubShell;
|
|
use App\Support\OperationRunType;
|
|
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 App\Support\Ui\DerivedState\DerivedStateFamily;
|
|
use App\Support\Ui\DerivedState\DerivedStateKey;
|
|
use App\Support\Ui\DerivedState\RequestScopedDerivedStateStore;
|
|
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,
|
|
private readonly RequestScopedDerivedStateStore $derivedStateStore,
|
|
) {}
|
|
|
|
/**
|
|
* @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->detailEntryObjects($sourceType, $record),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @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 detailEntriesFresh(string $sourceType, Model $record): array
|
|
{
|
|
return array_map(
|
|
static fn (RelatedContextEntry $entry): array => $entry->toArray(),
|
|
$this->detailEntryObjects($sourceType, $record, fresh: true),
|
|
);
|
|
}
|
|
|
|
public function primaryListAction(string $sourceType, Model $record): ?RelatedContextEntry
|
|
{
|
|
return $this->resolvePrimaryListAction($sourceType, $record);
|
|
}
|
|
|
|
public function primaryListActionFresh(string $sourceType, Model $record): ?RelatedContextEntry
|
|
{
|
|
return $this->resolvePrimaryListAction($sourceType, $record, fresh: true);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public function operationLinks(OperationRun $run, ?ManagedEnvironment $tenant): array
|
|
{
|
|
$entries = array_filter(
|
|
$this->detailEntryObjects(CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN, $run),
|
|
static fn (RelatedContextEntry $entry): bool => $entry->isAvailable(),
|
|
);
|
|
|
|
$links = [];
|
|
|
|
foreach ($entries as $entry) {
|
|
$links[$entry->actionLabel] = (string) $entry->targetUrl;
|
|
}
|
|
|
|
if ($tenant instanceof ManagedEnvironment) {
|
|
$links = [OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant)] + $links;
|
|
} else {
|
|
$links = [OperationRunLinks::collectionLabel() => OperationRunLinks::index()] + $links;
|
|
}
|
|
|
|
return $links;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public function operationLinksFresh(OperationRun $run, ?ManagedEnvironment $tenant): array
|
|
{
|
|
$entries = array_filter(
|
|
$this->detailEntryObjects(CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN, $run, fresh: true),
|
|
static fn (RelatedContextEntry $entry): bool => $entry->isAvailable(),
|
|
);
|
|
|
|
$links = [];
|
|
|
|
foreach ($entries as $entry) {
|
|
$links[$entry->actionLabel] = (string) $entry->targetUrl;
|
|
}
|
|
|
|
if ($tenant instanceof ManagedEnvironment) {
|
|
return [OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant)] + $links;
|
|
}
|
|
|
|
return [OperationRunLinks::collectionLabel() => OperationRunLinks::index()] + $links;
|
|
}
|
|
|
|
/**
|
|
* @return list<RelatedContextEntry>
|
|
*/
|
|
public function headerEntries(string $sourceType, Model $record): array
|
|
{
|
|
return $this->headerEntryObjects($sourceType, $record);
|
|
}
|
|
|
|
/**
|
|
* @return list<RelatedContextEntry>
|
|
*/
|
|
public function headerEntriesFresh(string $sourceType, Model $record): array
|
|
{
|
|
return $this->headerEntryObjects($sourceType, $record, fresh: true);
|
|
}
|
|
|
|
/**
|
|
* @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' => OperationRunLinks::openLabel(), 'url' => OperationRunLinks::tenantlessView($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 ManagedEnvironment
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_VIEW)
|
|
&& BackupSet::query()
|
|
->whereKey($resourceId)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->exists()
|
|
? ['label' => 'Open backup set', 'url' => BackupSetResource::getUrl('view', ['record' => $resourceId], tenant: $tenant)]
|
|
: null,
|
|
'restore_run' => $tenant instanceof ManagedEnvironment
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_VIEW)
|
|
&& RestoreRun::query()
|
|
->whereKey($resourceId)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->exists()
|
|
? ['label' => 'Open restore run', 'url' => RestoreRunResource::getUrl('view', ['record' => $resourceId], tenant: $tenant)]
|
|
: null,
|
|
'finding' => $tenant instanceof ManagedEnvironment
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_FINDINGS_VIEW)
|
|
&& Finding::query()
|
|
->whereKey($resourceId)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->exists()
|
|
? ['label' => 'Open finding', 'url' => FindingResource::getUrl('view', ['record' => $resourceId], tenant: $tenant)]
|
|
: null,
|
|
'finding_exception' => $tenant instanceof ManagedEnvironment
|
|
&& $this->capabilityResolver->isMember($user, $tenant)
|
|
&& $this->capabilityResolver->can($user, $tenant, Capabilities::FINDING_EXCEPTION_VIEW)
|
|
&& ($findingException = FindingException::query()
|
|
->whereKey($resourceId)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->first()) instanceof FindingException
|
|
? ['label' => 'Open finding exception', 'url' => FindingExceptionResource::getUrl('view', ['record' => $findingException], 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;
|
|
}
|
|
|
|
/**
|
|
* @return list<RelatedContextEntry>
|
|
*/
|
|
private function detailEntryObjects(string $sourceType, Model $record, bool $fresh = false): array
|
|
{
|
|
return $this->memoizedEntries(
|
|
family: DerivedStateFamily::RelatedNavigationDetail,
|
|
sourceType: $sourceType,
|
|
record: $record,
|
|
surface: CrossResourceNavigationMatrix::SURFACE_DETAIL_SECTION,
|
|
fresh: $fresh,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return list<RelatedContextEntry>
|
|
*/
|
|
private function headerEntryObjects(string $sourceType, Model $record, bool $fresh = false): array
|
|
{
|
|
return $this->memoizedEntries(
|
|
family: DerivedStateFamily::RelatedNavigationHeader,
|
|
sourceType: $sourceType,
|
|
record: $record,
|
|
surface: CrossResourceNavigationMatrix::SURFACE_DETAIL_HEADER,
|
|
fresh: $fresh,
|
|
);
|
|
}
|
|
|
|
private function resolvePrimaryListAction(string $sourceType, Model $record, bool $fresh = false): ?RelatedContextEntry
|
|
{
|
|
$entries = array_values(array_filter(
|
|
$this->memoizedEntries(
|
|
family: DerivedStateFamily::RelatedNavigationPrimary,
|
|
sourceType: $sourceType,
|
|
record: $record,
|
|
surface: CrossResourceNavigationMatrix::SURFACE_LIST_ROW,
|
|
fresh: $fresh,
|
|
),
|
|
static fn (RelatedContextEntry $entry): bool => $entry->isAvailable(),
|
|
));
|
|
|
|
return $entries[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* @return list<RelatedContextEntry>
|
|
*/
|
|
private function memoizedEntries(
|
|
DerivedStateFamily $family,
|
|
string $sourceType,
|
|
Model $record,
|
|
string $surface,
|
|
bool $fresh = false,
|
|
): array {
|
|
$key = DerivedStateKey::fromModel(
|
|
family: $family,
|
|
record: $record,
|
|
variant: $sourceType,
|
|
context: [
|
|
'source_type' => $sourceType,
|
|
'surface' => $surface,
|
|
'active_tenant_id' => $this->activeEnvironmentId(),
|
|
'route_name' => request()?->route()?->getName(),
|
|
'user_id' => auth()->id(),
|
|
],
|
|
);
|
|
|
|
/** @var list<RelatedContextEntry> $entries */
|
|
$entries = $fresh
|
|
? $this->derivedStateStore->resolveFresh(
|
|
$key,
|
|
fn (): array => $this->resolveEntries($sourceType, $surface, $record),
|
|
$family->defaultFreshnessPolicy(),
|
|
$family->allowsNegativeResultCache(),
|
|
)
|
|
: $this->derivedStateStore->resolve(
|
|
$key,
|
|
fn (): array => $this->resolveEntries($sourceType, $surface, $record),
|
|
$family->defaultFreshnessPolicy(),
|
|
$family->allowsNegativeResultCache(),
|
|
);
|
|
|
|
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->managed_environment_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: $run->reconciledRelatedBackupSetId()
|
|
?? (is_numeric($context['backup_set_id'] ?? null) ? (int) $context['backup_set_id'] : null),
|
|
tenantId: is_numeric($run->managed_environment_id ?? null) ? (int) $run->managed_environment_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->managed_environment_id ?? null) ? (int) $run->managed_environment_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: $run->reconciledRelatedBaselineSnapshotId()
|
|
?? (is_numeric($context['baseline_snapshot_id'] ?? null) ? (int) $context['baseline_snapshot_id'] : null),
|
|
workspaceId: (int) $run->workspace_id,
|
|
),
|
|
'parent_policy' => $this->operationRunPolicyEntry($rule, $run),
|
|
'evidence_snapshot' => $this->evidenceSnapshotEntry($rule, $run),
|
|
'environment_review' => $this->environmentReviewEntry($rule, $run),
|
|
'review_pack' => $this->reviewPackEntry($rule, $run),
|
|
'baseline_subject_resolution' => $this->baselineSubjectResolutionEntry($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: $profile->resolveCurrentConsumableSnapshot()?->getKey(),
|
|
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->activeEnvironmentId(),
|
|
);
|
|
}
|
|
|
|
private function backupSetRunEntry(NavigationMatrixRule $rule, BackupSet $backupSet): ?RelatedContextEntry
|
|
{
|
|
$candidate = OperationRun::query()
|
|
->where('managed_environment_id', (int) $backupSet->managed_environment_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('managed_environment_id', (int) $run->managed_environment_id)
|
|
->first();
|
|
|
|
return $this->policyEntry($rule, $policy);
|
|
}
|
|
|
|
private function environmentReviewEntry(NavigationMatrixRule $rule, OperationRun $run): ?RelatedContextEntry
|
|
{
|
|
$reviewId = $run->reconciledRelatedReviewId();
|
|
$tenant = $run->tenant;
|
|
|
|
if ($reviewId === null || $reviewId <= 0 || ! $tenant instanceof ManagedEnvironment) {
|
|
return null;
|
|
}
|
|
|
|
$review = EnvironmentReview::query()
|
|
->whereKey($reviewId)
|
|
->where('workspace_id', (int) $run->workspace_id)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->first();
|
|
|
|
if (! $review instanceof EnvironmentReview) {
|
|
return $this->unavailableEntry($rule, '#'.$reviewId, 'missing');
|
|
}
|
|
|
|
if (! $this->canOpenTenantRecord($tenant, Capabilities::ENVIRONMENT_REVIEW_VIEW)) {
|
|
return $this->unavailableEntry($rule, '#'.$reviewId, 'unauthorized');
|
|
}
|
|
|
|
return RelatedContextEntry::available(
|
|
key: $rule->relationKey,
|
|
label: $this->labels->entryLabel($rule->relationKey),
|
|
value: 'Review #'.$review->getKey(),
|
|
secondaryValue: ucfirst(str_replace('_', ' ', (string) $review->status)),
|
|
targetUrl: EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant),
|
|
targetKind: $rule->targetType,
|
|
priority: $rule->priority,
|
|
actionLabel: $this->labels->actionLabel($rule->relationKey),
|
|
contextBadge: 'ManagedEnvironment',
|
|
);
|
|
}
|
|
|
|
private function evidenceSnapshotEntry(NavigationMatrixRule $rule, OperationRun $run): ?RelatedContextEntry
|
|
{
|
|
$snapshotId = $run->reconciledRelatedEvidenceSnapshotId();
|
|
$tenant = $run->tenant;
|
|
|
|
if ($snapshotId === null || $snapshotId <= 0 || ! $tenant instanceof ManagedEnvironment) {
|
|
return null;
|
|
}
|
|
|
|
$snapshot = EvidenceSnapshot::query()
|
|
->whereKey($snapshotId)
|
|
->where('workspace_id', (int) $run->workspace_id)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->first();
|
|
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
return $this->unavailableEntry($rule, '#'.$snapshotId, 'missing');
|
|
}
|
|
|
|
if (! $this->canOpenTenantRecord($tenant, Capabilities::EVIDENCE_VIEW)) {
|
|
return $this->unavailableEntry($rule, '#'.$snapshotId, 'unauthorized');
|
|
}
|
|
|
|
return RelatedContextEntry::available(
|
|
key: $rule->relationKey,
|
|
label: $this->labels->entryLabel($rule->relationKey),
|
|
value: 'Snapshot #'.$snapshot->getKey(),
|
|
secondaryValue: ucfirst(str_replace('_', ' ', (string) $snapshot->status)),
|
|
targetUrl: EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant),
|
|
targetKind: $rule->targetType,
|
|
priority: $rule->priority,
|
|
actionLabel: $this->labels->actionLabel($rule->relationKey),
|
|
contextBadge: 'Evidence',
|
|
);
|
|
}
|
|
|
|
private function reviewPackEntry(NavigationMatrixRule $rule, OperationRun $run): ?RelatedContextEntry
|
|
{
|
|
$reviewPackId = $run->reconciledRelatedReviewPackId();
|
|
$tenant = $run->tenant;
|
|
|
|
if ($reviewPackId === null || $reviewPackId <= 0 || ! $tenant instanceof ManagedEnvironment) {
|
|
return null;
|
|
}
|
|
|
|
$pack = ReviewPack::query()
|
|
->whereKey($reviewPackId)
|
|
->where('workspace_id', (int) $run->workspace_id)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->first();
|
|
|
|
if (! $pack instanceof ReviewPack) {
|
|
return $this->unavailableEntry($rule, '#'.$reviewPackId, 'missing');
|
|
}
|
|
|
|
if (! $this->canOpenTenantRecord($tenant, Capabilities::REVIEW_PACK_VIEW)) {
|
|
return $this->unavailableEntry($rule, '#'.$reviewPackId, 'unauthorized');
|
|
}
|
|
|
|
return RelatedContextEntry::available(
|
|
key: $rule->relationKey,
|
|
label: $this->labels->entryLabel($rule->relationKey),
|
|
value: 'Review pack #'.$pack->getKey(),
|
|
secondaryValue: ucfirst(str_replace('_', ' ', (string) $pack->status)),
|
|
targetUrl: ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant),
|
|
targetKind: $rule->targetType,
|
|
priority: $rule->priority,
|
|
actionLabel: $this->labels->actionLabel($rule->relationKey),
|
|
contextBadge: 'Reporting',
|
|
);
|
|
}
|
|
|
|
private function baselineSubjectResolutionEntry(NavigationMatrixRule $rule, OperationRun $run): ?RelatedContextEntry
|
|
{
|
|
$tenant = $run->tenant;
|
|
|
|
if (
|
|
! $tenant instanceof ManagedEnvironment
|
|
|| $run->canonicalOperationType() !== OperationRunType::BaselineCompare->value
|
|
|| ! $this->canOpenTenantRecord($tenant, Capabilities::WORKSPACE_BASELINES_VIEW)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
$summary = app(BaselineSubjectResolutionQuery::class)->summary($tenant, (int) $run->getKey());
|
|
|
|
if ((int) ($summary['actionable_count'] ?? 0) <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return RelatedContextEntry::available(
|
|
key: $rule->relationKey,
|
|
label: $this->labels->entryLabel($rule->relationKey),
|
|
value: 'Baseline subject resolution',
|
|
secondaryValue: OperationRunLinks::identifier($run),
|
|
targetUrl: ManagedEnvironmentLinks::baselineSubjectResolutionUrl($tenant, [
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
]),
|
|
targetKind: $rule->targetType,
|
|
priority: $rule->priority,
|
|
actionLabel: $this->labels->actionLabel($rule->relationKey),
|
|
contextBadge: 'Baseline compare',
|
|
);
|
|
}
|
|
|
|
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('managed_environment_id', (int) $finding->managed_environment_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->managed_environment_id ?? null) ? (int) $policy->managed_environment_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('managed_environment_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: 'ManagedEnvironment',
|
|
);
|
|
}
|
|
|
|
private function operationsEntry(
|
|
NavigationMatrixRule $rule,
|
|
?ManagedEnvironment $tenant,
|
|
?CanonicalNavigationContext $context = null,
|
|
): ?RelatedContextEntry {
|
|
if (! $tenant instanceof ManagedEnvironment) {
|
|
return null;
|
|
}
|
|
|
|
if (! $this->canOpenTenantRecord($tenant, Capabilities::TENANT_VIEW)) {
|
|
return null;
|
|
}
|
|
|
|
return RelatedContextEntry::available(
|
|
key: $rule->relationKey,
|
|
label: $this->labels->entryLabel($rule->relationKey),
|
|
value: OperationRunLinks::collectionLabel(),
|
|
secondaryValue: $tenant->name,
|
|
targetUrl: OperationRunLinks::index($tenant, $context),
|
|
targetKind: $rule->targetType,
|
|
priority: $rule->priority,
|
|
actionLabel: $this->labels->actionLabel($rule->relationKey),
|
|
contextBadge: 'Environment 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(?ManagedEnvironment $tenant, string $capability): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
return $tenant instanceof ManagedEnvironment
|
|
&& $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 ManagedEnvironment || ! $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,
|
|
);
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
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),
|
|
);
|
|
}
|
|
|
|
private function activeEnvironmentId(): ?int
|
|
{
|
|
$tenant = app(OperateHubShell::class)->activeEntitledTenant(request());
|
|
|
|
return $tenant instanceof ManagedEnvironment ? (int) $tenant->getKey() : null;
|
|
}
|
|
}
|