Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m12s
Replaced legacy tenant and environment bindings in the BaselineDriftEngine with the new ProviderResourceIdentity framework as defined in Spec 382.
448 lines
17 KiB
PHP
448 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Baselines\Matching;
|
|
|
|
use App\Models\ProviderResourceBinding;
|
|
use App\Support\Baselines\BaselineSubjectKey;
|
|
use App\Support\Baselines\Matching\BaselineSubjectDescriptor;
|
|
use App\Support\Baselines\Matching\MatchingOutcome;
|
|
use App\Support\Resources\ProviderResourceBindingStatus;
|
|
use App\Support\Resources\ProviderResourceDescriptor;
|
|
use App\Support\Resources\ProviderResourceResolutionMode;
|
|
use App\Support\Resources\ResourceIdentity;
|
|
|
|
final class SubjectMatchingPipeline
|
|
{
|
|
public function __construct(
|
|
private readonly FoundationCoverageResolver $foundationCoverageResolver,
|
|
) {}
|
|
|
|
/**
|
|
* @param list<BaselineSubjectDescriptor> $baselineSubjects
|
|
* @param list<ProviderResourceDescriptor> $currentDescriptors
|
|
* @return array{
|
|
* outcomes: list<MatchingOutcome>,
|
|
* diagnostics: array<string, mixed>
|
|
* }
|
|
*/
|
|
public function matchAll(int $workspaceId, int $managedEnvironmentId, array $baselineSubjects, array $currentDescriptors): array
|
|
{
|
|
$currentIndex = $this->indexCurrentDescriptors($currentDescriptors);
|
|
$bindingIndex = $this->activeBindingsByCanonicalKey($workspaceId, $managedEnvironmentId, $baselineSubjects);
|
|
$outcomes = [];
|
|
|
|
foreach ($baselineSubjects as $subject) {
|
|
$outcomes[] = $this->matchOne($subject, $currentIndex, $bindingIndex);
|
|
}
|
|
|
|
return [
|
|
'outcomes' => $outcomes,
|
|
'diagnostics' => $this->diagnostics($outcomes, $bindingIndex),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* by_canonical_key: array<string, list<ProviderResourceDescriptor>>,
|
|
* by_identity_key: array<string, list<ProviderResourceDescriptor>>
|
|
* } $currentIndex
|
|
* @param array<string, list<ProviderResourceBinding>> $bindingIndex
|
|
*/
|
|
private function matchOne(BaselineSubjectDescriptor $subject, array $currentIndex, array $bindingIndex): MatchingOutcome
|
|
{
|
|
$canonicalKey = $this->subjectCanonicalKey($subject);
|
|
|
|
if ($canonicalKey !== null && isset($bindingIndex[$canonicalKey])) {
|
|
$bindings = $bindingIndex[$canonicalKey];
|
|
|
|
if (count($bindings) !== 1) {
|
|
return MatchingOutcome::ambiguous($subject, [
|
|
'match_stage' => 'active_binding',
|
|
'canonical_subject_key' => $canonicalKey,
|
|
'binding_count' => count($bindings),
|
|
]);
|
|
}
|
|
|
|
return $this->matchBinding($subject, $bindings[0], $currentIndex);
|
|
}
|
|
|
|
if ($canonicalKey !== null) {
|
|
$byCanonical = $currentIndex['by_canonical_key'][$canonicalKey] ?? [];
|
|
|
|
if (count($byCanonical) === 1) {
|
|
return MatchingOutcome::resolved(
|
|
subject: $subject,
|
|
matchedDescriptor: $byCanonical[0],
|
|
matchedSubjectKey: $canonicalKey,
|
|
reasonCode: 'canonical_subject_key',
|
|
trust: 'high',
|
|
proof: [
|
|
'match_stage' => 'canonical_subject_key',
|
|
'canonical_subject_key' => $canonicalKey,
|
|
],
|
|
);
|
|
}
|
|
|
|
if (count($byCanonical) > 1) {
|
|
return MatchingOutcome::ambiguous($subject, [
|
|
'match_stage' => 'canonical_subject_key',
|
|
'canonical_subject_key' => $canonicalKey,
|
|
'candidate_count' => count($byCanonical),
|
|
]);
|
|
}
|
|
}
|
|
|
|
if ($subject->providerResourceDescriptor instanceof ProviderResourceDescriptor) {
|
|
$byCanonical = $this->currentByCanonicalSubject($subject->providerResourceDescriptor, $currentIndex);
|
|
|
|
if (count($byCanonical) === 1) {
|
|
return MatchingOutcome::resolved(
|
|
subject: $subject,
|
|
matchedDescriptor: $byCanonical[0],
|
|
matchedSubjectKey: $canonicalKey ?? $this->currentCanonicalKey($byCanonical[0]) ?? $subject->comparisonSubjectKey(),
|
|
reasonCode: 'provider_identity',
|
|
trust: 'high',
|
|
proof: [
|
|
'match_stage' => 'provider_identity',
|
|
'canonical_subject_key' => $canonicalKey,
|
|
],
|
|
);
|
|
}
|
|
|
|
if (count($byCanonical) > 1) {
|
|
return MatchingOutcome::ambiguous($subject, [
|
|
'match_stage' => 'provider_identity',
|
|
'canonical_subject_key' => $canonicalKey,
|
|
'candidate_count' => count($byCanonical),
|
|
]);
|
|
}
|
|
|
|
}
|
|
|
|
$coverageOutcome = $this->foundationCoverageOutcome($subject);
|
|
|
|
if ($coverageOutcome instanceof MatchingOutcome) {
|
|
return $coverageOutcome;
|
|
}
|
|
|
|
if ($canonicalKey !== null || $subject->providerResourceDescriptor instanceof ProviderResourceDescriptor) {
|
|
return MatchingOutcome::missingLocalEvidence($subject, [
|
|
'match_stage' => $canonicalKey !== null ? 'canonical_subject_key' : 'provider_identity',
|
|
'canonical_subject_key' => $canonicalKey,
|
|
]);
|
|
}
|
|
|
|
return MatchingOutcome::unresolvedIdentity($subject, [
|
|
'match_stage' => 'identity_required',
|
|
]);
|
|
}
|
|
|
|
private function foundationCoverageOutcome(BaselineSubjectDescriptor $subject): ?MatchingOutcome
|
|
{
|
|
$coverage = $this->foundationCoverageResolver->coverageFor(
|
|
$subject->subjectTypeKey,
|
|
$subject->providerResourceDescriptor?->identity,
|
|
);
|
|
|
|
$proof = [
|
|
'match_stage' => 'foundation_coverage',
|
|
'coverage' => $coverage['coverage'],
|
|
'support_mode' => $coverage['support_mode'],
|
|
'source_model_expected' => $coverage['source_model_expected'],
|
|
'identity_kind' => $coverage['identity_kind'],
|
|
];
|
|
|
|
return match ($coverage['coverage']) {
|
|
'unsupported' => MatchingOutcome::unsupported($subject, $proof + [
|
|
'reason_code' => $coverage['reason_code'] ?? 'unsupported_subject',
|
|
]),
|
|
'inventory_only' => MatchingOutcome::limited(
|
|
subject: $subject,
|
|
reasonCode: $coverage['reason_code'] ?? 'foundation_not_policy_backed',
|
|
proof: $proof,
|
|
),
|
|
'identity_only', 'canonical_only' => MatchingOutcome::limited(
|
|
subject: $subject,
|
|
reasonCode: $coverage['reason_code'] ?? 'accepted_limitation',
|
|
proof: $proof,
|
|
),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* by_canonical_key: array<string, list<ProviderResourceDescriptor>>,
|
|
* by_identity_key: array<string, list<ProviderResourceDescriptor>>
|
|
* } $currentIndex
|
|
*/
|
|
private function matchBinding(BaselineSubjectDescriptor $subject, ProviderResourceBinding $binding, array $currentIndex): MatchingOutcome
|
|
{
|
|
$proof = [
|
|
'match_stage' => 'active_binding',
|
|
'provider_resource_binding_id' => (int) $binding->getKey(),
|
|
'provider_key' => (string) $binding->provider_key,
|
|
'canonical_subject_key' => (string) $binding->canonical_subject_key,
|
|
'resolution_mode' => $this->resolutionModeValue($binding),
|
|
'binding_status' => $this->bindingStatusValue($binding),
|
|
];
|
|
|
|
return match ($this->resolutionModeValue($binding)) {
|
|
ProviderResourceResolutionMode::ExcludedNonGoverned->value => MatchingOutcome::excluded($subject, $proof),
|
|
ProviderResourceResolutionMode::AcceptedLimitation->value => MatchingOutcome::limited($subject, 'accepted_limitation', $proof),
|
|
ProviderResourceResolutionMode::UnsupportedCoverage->value => MatchingOutcome::unsupported($subject, $proof),
|
|
ProviderResourceResolutionMode::MissingExpected->value => MatchingOutcome::missingProviderResource($subject, $proof),
|
|
default => $this->matchActiveBindingToCurrentDescriptor($subject, $binding, $currentIndex, $proof),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* by_canonical_key: array<string, list<ProviderResourceDescriptor>>,
|
|
* by_identity_key: array<string, list<ProviderResourceDescriptor>>
|
|
* } $currentIndex
|
|
* @param array<string, string|int|float|bool|null> $proof
|
|
*/
|
|
private function matchActiveBindingToCurrentDescriptor(BaselineSubjectDescriptor $subject, ProviderResourceBinding $binding, array $currentIndex, array $proof): MatchingOutcome
|
|
{
|
|
$candidates = $currentIndex['by_canonical_key'][(string) $binding->canonical_subject_key] ?? [];
|
|
|
|
if ($candidates === []) {
|
|
$identityKey = $this->bindingIdentityKey($binding);
|
|
$candidates = $identityKey !== null
|
|
? ($currentIndex['by_identity_key'][$identityKey] ?? [])
|
|
: [];
|
|
}
|
|
|
|
if (count($candidates) === 1) {
|
|
return MatchingOutcome::resolved(
|
|
subject: $subject,
|
|
matchedDescriptor: $candidates[0],
|
|
matchedSubjectKey: (string) $binding->canonical_subject_key,
|
|
reasonCode: 'active_provider_resource_binding',
|
|
trust: 'authoritative',
|
|
proof: $proof,
|
|
);
|
|
}
|
|
|
|
if (count($candidates) > 1) {
|
|
return MatchingOutcome::ambiguous($subject, $proof + [
|
|
'candidate_count' => count($candidates),
|
|
]);
|
|
}
|
|
|
|
return MatchingOutcome::missingLocalEvidence($subject, $proof);
|
|
}
|
|
|
|
/**
|
|
* @param list<ProviderResourceDescriptor> $currentDescriptors
|
|
* @return array{
|
|
* by_canonical_key: array<string, list<ProviderResourceDescriptor>>,
|
|
* by_identity_key: array<string, list<ProviderResourceDescriptor>>
|
|
* }
|
|
*/
|
|
private function indexCurrentDescriptors(array $currentDescriptors): array
|
|
{
|
|
$index = [
|
|
'by_canonical_key' => [],
|
|
'by_identity_key' => [],
|
|
];
|
|
|
|
foreach ($currentDescriptors as $descriptor) {
|
|
if (! $descriptor instanceof ProviderResourceDescriptor) {
|
|
continue;
|
|
}
|
|
|
|
$canonicalKey = $this->currentCanonicalKey($descriptor);
|
|
|
|
if ($canonicalKey !== null) {
|
|
$index['by_canonical_key'][$canonicalKey][] = $descriptor;
|
|
}
|
|
|
|
$identityKey = $this->descriptorIdentityKey($descriptor);
|
|
|
|
if ($identityKey !== null) {
|
|
$index['by_identity_key'][$identityKey][] = $descriptor;
|
|
}
|
|
}
|
|
|
|
return $index;
|
|
}
|
|
|
|
/**
|
|
* @param list<BaselineSubjectDescriptor> $baselineSubjects
|
|
* @return array<string, list<ProviderResourceBinding>>
|
|
*/
|
|
private function activeBindingsByCanonicalKey(int $workspaceId, int $managedEnvironmentId, array $baselineSubjects): array
|
|
{
|
|
$keys = [];
|
|
|
|
foreach ($baselineSubjects as $subject) {
|
|
if (! $subject instanceof BaselineSubjectDescriptor) {
|
|
continue;
|
|
}
|
|
|
|
$canonicalKey = $this->subjectCanonicalKey($subject);
|
|
|
|
if ($canonicalKey !== null) {
|
|
$keys[$canonicalKey] = true;
|
|
}
|
|
}
|
|
|
|
if ($keys === []) {
|
|
return [];
|
|
}
|
|
|
|
$bindings = ProviderResourceBinding::query()
|
|
->where('workspace_id', $workspaceId)
|
|
->where('managed_environment_id', $managedEnvironmentId)
|
|
->whereIn('canonical_subject_key', array_keys($keys))
|
|
->where('binding_status', ProviderResourceBindingStatus::Active->value)
|
|
->orderBy('id')
|
|
->get();
|
|
|
|
$index = [];
|
|
|
|
foreach ($bindings as $binding) {
|
|
if (! $binding instanceof ProviderResourceBinding) {
|
|
continue;
|
|
}
|
|
|
|
$index[(string) $binding->canonical_subject_key][] = $binding;
|
|
}
|
|
|
|
return $index;
|
|
}
|
|
|
|
private function subjectCanonicalKey(BaselineSubjectDescriptor $subject): ?string
|
|
{
|
|
if (BaselineSubjectKey::isProviderResourceCanonicalKey($subject->canonicalSubjectKey)) {
|
|
return $subject->canonicalSubjectKey;
|
|
}
|
|
|
|
if (! $subject->providerResourceDescriptor instanceof ProviderResourceDescriptor) {
|
|
return null;
|
|
}
|
|
|
|
return BaselineSubjectKey::forProviderResourceIdentity(
|
|
subjectDomain: $subject->providerResourceDescriptor->subjectDomain,
|
|
subjectClass: $subject->providerResourceDescriptor->subjectClass,
|
|
subjectTypeKey: $subject->providerResourceDescriptor->subjectTypeKey,
|
|
identity: $subject->providerResourceDescriptor->identity,
|
|
);
|
|
}
|
|
|
|
private function currentCanonicalKey(ProviderResourceDescriptor $descriptor): ?string
|
|
{
|
|
return BaselineSubjectKey::forProviderResourceIdentity(
|
|
subjectDomain: $descriptor->subjectDomain,
|
|
subjectClass: $descriptor->subjectClass,
|
|
subjectTypeKey: $descriptor->subjectTypeKey,
|
|
identity: $descriptor->identity,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* by_canonical_key: array<string, list<ProviderResourceDescriptor>>
|
|
* } $currentIndex
|
|
* @return list<ProviderResourceDescriptor>
|
|
*/
|
|
private function currentByCanonicalSubject(ProviderResourceDescriptor $descriptor, array $currentIndex): array
|
|
{
|
|
$canonicalKey = $this->currentCanonicalKey($descriptor);
|
|
|
|
return $canonicalKey !== null
|
|
? ($currentIndex['by_canonical_key'][$canonicalKey] ?? [])
|
|
: [];
|
|
}
|
|
|
|
private function descriptorIdentityKey(ProviderResourceDescriptor $descriptor): ?string
|
|
{
|
|
return $this->identityKey($descriptor->identity);
|
|
}
|
|
|
|
private function bindingIdentityKey(ProviderResourceBinding $binding): ?string
|
|
{
|
|
$identityKind = is_string($binding->provider_resource_identity_kind) ? trim($binding->provider_resource_identity_kind) : '';
|
|
$providerKey = is_string($binding->provider_key) ? trim($binding->provider_key) : '';
|
|
$resourceType = is_string($binding->provider_resource_type) ? trim($binding->provider_resource_type) : '';
|
|
$stableIdentity = $identityKind === ResourceIdentity::ProviderResource
|
|
? (is_string($binding->provider_resource_id) ? trim($binding->provider_resource_id) : '')
|
|
: (is_string($binding->provider_resource_discriminator) ? trim($binding->provider_resource_discriminator) : '');
|
|
|
|
if ($providerKey === '' || $resourceType === '' || $identityKind === '' || $stableIdentity === '') {
|
|
return null;
|
|
}
|
|
|
|
return implode('|', [$providerKey, $resourceType, $identityKind, $stableIdentity]);
|
|
}
|
|
|
|
private function identityKey(ResourceIdentity $identity): ?string
|
|
{
|
|
$stableIdentity = $identity->stableIdentityValue();
|
|
|
|
if ($identity->providerResourceType === null || $stableIdentity === null) {
|
|
return null;
|
|
}
|
|
|
|
return implode('|', [
|
|
$identity->providerKey,
|
|
$identity->providerResourceType,
|
|
$identity->identityKind,
|
|
$stableIdentity,
|
|
]);
|
|
}
|
|
|
|
private function resolutionModeValue(ProviderResourceBinding $binding): string
|
|
{
|
|
return $binding->resolution_mode instanceof ProviderResourceResolutionMode
|
|
? $binding->resolution_mode->value
|
|
: (string) $binding->resolution_mode;
|
|
}
|
|
|
|
private function bindingStatusValue(ProviderResourceBinding $binding): string
|
|
{
|
|
return $binding->binding_status instanceof ProviderResourceBindingStatus
|
|
? $binding->binding_status->value
|
|
: (string) $binding->binding_status;
|
|
}
|
|
|
|
/**
|
|
* @param list<MatchingOutcome> $outcomes
|
|
* @param array<string, list<ProviderResourceBinding>> $bindingIndex
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function diagnostics(array $outcomes, array $bindingIndex): array
|
|
{
|
|
$byStatus = [];
|
|
$byReason = [];
|
|
$activeBindingsConsidered = 0;
|
|
|
|
foreach ($bindingIndex as $bindings) {
|
|
$activeBindingsConsidered += count($bindings);
|
|
}
|
|
|
|
foreach ($outcomes as $outcome) {
|
|
if (! $outcome instanceof MatchingOutcome) {
|
|
continue;
|
|
}
|
|
|
|
$byStatus[$outcome->status] = ($byStatus[$outcome->status] ?? 0) + 1;
|
|
$byReason[$outcome->reasonCode] = ($byReason[$outcome->reasonCode] ?? 0) + 1;
|
|
}
|
|
|
|
ksort($byStatus);
|
|
ksort($byReason);
|
|
|
|
return [
|
|
'subjects_total' => count($outcomes),
|
|
'active_bindings_considered' => $activeBindingsConsidered,
|
|
'by_status' => $byStatus,
|
|
'by_reason' => $byReason,
|
|
];
|
|
}
|
|
}
|