Added `ProviderResourceBinding` model, migrations, policies, and supporting framework for canonical resource identity mapping as defined in Spec 381. This provides the structural capability to resolve baseline and posture discrepancies by binding logical entities across source providers to canonical identities. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #452
189 lines
10 KiB
PHP
189 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\OperationRun;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderResourceBinding;
|
|
use App\Services\Resources\ProviderResourceBindingService;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Baselines\SubjectClass;
|
|
use App\Support\Resources\ProviderResourceBindingStatus;
|
|
use App\Support\Resources\ProviderResourceResolutionMode;
|
|
use App\Support\Resources\ResourceIdentity;
|
|
|
|
function providerResourceBindingAttributes(array $overrides = []): array
|
|
{
|
|
return array_replace([
|
|
'subject_domain' => 'baseline',
|
|
'subject_class' => SubjectClass::PolicyBacked,
|
|
'subject_type_key' => 'deviceConfiguration',
|
|
'operator_note' => 'Operator confirmed this provider resource binding after reviewing duplicate labels.',
|
|
], $overrides);
|
|
}
|
|
|
|
it('creates manual fake-provider bindings with scoped references and safe audit metadata', function (): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$connection = ProviderConnection::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'provider' => 'fake-provider',
|
|
]);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create();
|
|
$inventory = InventoryItem::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
]);
|
|
$policyVersion = PolicyVersion::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
]);
|
|
[, $snapshot] = seedActiveBaselineForTenant($tenant);
|
|
|
|
$binding = app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: ResourceIdentity::providerResource('fake-provider', 'policy', 'fake-policy-1'),
|
|
attributes: providerResourceBindingAttributes([
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'source_operation_run_id' => (int) $run->getKey(),
|
|
'source_inventory_item_id' => (int) $inventory->getKey(),
|
|
'source_policy_version_id' => (int) $policyVersion->getKey(),
|
|
'source_baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'display_label' => 'Duplicate policy label',
|
|
'resolution_reason' => 'duplicate-provider-resource',
|
|
]),
|
|
);
|
|
|
|
expect((int) $binding->workspace_id)->toBe((int) $tenant->workspace_id)
|
|
->and((int) $binding->managed_environment_id)->toBe((int) $tenant->getKey())
|
|
->and($binding->provider_key)->toBe('fake-provider')
|
|
->and($binding->resolution_mode)->toBe(ProviderResourceResolutionMode::ManualBinding)
|
|
->and($binding->binding_status)->toBe(ProviderResourceBindingStatus::Active)
|
|
->and((int) $binding->provider_connection_id)->toBe((int) $connection->getKey())
|
|
->and((int) $binding->source_operation_run_id)->toBe((int) $run->getKey())
|
|
->and((int) $binding->source_inventory_item_id)->toBe((int) $inventory->getKey())
|
|
->and((int) $binding->source_policy_version_id)->toBe((int) $policyVersion->getKey())
|
|
->and((int) $binding->source_baseline_snapshot_id)->toBe((int) $snapshot->getKey());
|
|
|
|
$audit = AuditLog::query()
|
|
->where('action', AuditActionId::ProviderResourceBindingCreated->value)
|
|
->where('resource_type', 'provider_resource_binding')
|
|
->firstOrFail();
|
|
|
|
expect((int) $audit->workspace_id)->toBe((int) $tenant->workspace_id)
|
|
->and((int) $audit->managed_environment_id)->toBe((int) $tenant->getKey())
|
|
->and(data_get($audit->metadata, 'provider_key'))->toBe('fake-provider')
|
|
->and(data_get($audit->metadata, 'resolution_mode'))->toBe(ProviderResourceResolutionMode::ManualBinding->value)
|
|
->and(data_get($audit->metadata, 'source_operation_run_id'))->toBe((int) $run->getKey())
|
|
->and(data_get($audit->metadata, 'operator_note'))->toBeNull()
|
|
->and(data_get($audit->metadata, 'operator_note_sha256'))->toBe(hash('sha256', providerResourceBindingAttributes()['operator_note']))
|
|
->and(data_get($audit->metadata, 'operator_note_length'))->toBe(mb_strlen(providerResourceBindingAttributes()['operator_note']));
|
|
});
|
|
|
|
it('supersedes an existing active binding and keeps exactly one active row', function (): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
$service = app(ProviderResourceBindingService::class);
|
|
$identity = ResourceIdentity::providerResource('fake-provider', 'policy', 'same-subject');
|
|
|
|
$first = $service->createExactProviderIdentity($actor, $tenant, $identity, providerResourceBindingAttributes([
|
|
'operator_note' => 'Initial identity binding.',
|
|
]));
|
|
|
|
$second = $service->createManualBinding($actor, $tenant, $identity, providerResourceBindingAttributes([
|
|
'operator_note' => 'Replacement identity binding after manual review.',
|
|
]));
|
|
|
|
expect($first->refresh()->binding_status)->toBe(ProviderResourceBindingStatus::Superseded)
|
|
->and($second->binding_status)->toBe(ProviderResourceBindingStatus::Active)
|
|
->and(ProviderResourceBinding::query()
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('provider_key', 'fake-provider')
|
|
->where('canonical_subject_key', $second->canonical_subject_key)
|
|
->active()
|
|
->count())->toBe(1);
|
|
|
|
$audit = AuditLog::query()
|
|
->where('action', AuditActionId::ProviderResourceBindingSuperseded->value)
|
|
->firstOrFail();
|
|
|
|
expect(data_get($audit->metadata, 'old_binding_id'))->toBe((int) $first->getKey())
|
|
->and(data_get($audit->metadata, 'new_binding_id'))->toBe((int) $second->getKey())
|
|
->and(data_get($audit->metadata, 'resolution_mode'))->toBe(ProviderResourceResolutionMode::ManualBinding->value);
|
|
});
|
|
|
|
it('revokes active bindings with audit metadata', function (): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$binding = app(ProviderResourceBindingService::class)->createAcceptedLimitation(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: ResourceIdentity::unsupported('fake-provider', 'foundation-resource', 'unsupported-subject'),
|
|
attributes: providerResourceBindingAttributes([
|
|
'operator_note' => 'Accepted limitation for unsupported coverage.',
|
|
'subject_class' => SubjectClass::FoundationBacked,
|
|
'subject_type_key' => 'foundationResource',
|
|
]),
|
|
);
|
|
|
|
$revoked = app(ProviderResourceBindingService::class)->revoke(
|
|
actor: $actor,
|
|
binding: $binding,
|
|
operatorNote: 'Revoked because the limitation is no longer accepted.',
|
|
);
|
|
|
|
expect($revoked->binding_status)->toBe(ProviderResourceBindingStatus::Revoked)
|
|
->and($revoked->ended_at)->not->toBeNull();
|
|
|
|
$audit = AuditLog::query()
|
|
->where('action', AuditActionId::ProviderResourceBindingRevoked->value)
|
|
->firstOrFail();
|
|
|
|
expect(data_get($audit->metadata, 'old_binding_id'))->toBe((int) $binding->getKey())
|
|
->and(data_get($audit->metadata, 'binding_status'))->toBe(ProviderResourceBindingStatus::Revoked->value)
|
|
->and(data_get($audit->metadata, 'operator_note'))->toBeNull();
|
|
});
|
|
|
|
it('requires operator notes for binding decisions', function (): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
expect(fn () => app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: ResourceIdentity::providerResource('fake-provider', 'policy', 'missing-note'),
|
|
attributes: providerResourceBindingAttributes(['operator_note' => '']),
|
|
))->toThrow(InvalidArgumentException::class, 'operator note');
|
|
});
|
|
|
|
it('persists every v1 resolution mode without Microsoft literals', function (string $method, ResourceIdentity $identity, ProviderResourceResolutionMode $mode): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$binding = app(ProviderResourceBindingService::class)->{$method}(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: $identity,
|
|
attributes: providerResourceBindingAttributes([
|
|
'subject_class' => SubjectClass::FoundationBacked,
|
|
'subject_type_key' => 'fakeSubjectType',
|
|
'operator_note' => 'Provider-neutral decision for '.$mode->value,
|
|
]),
|
|
);
|
|
|
|
expect($binding->provider_key)->toBe('fake-provider')
|
|
->and($binding->resolution_mode)->toBe($mode)
|
|
->and($binding->canonical_subject_key)->toContain('fake-provider')
|
|
->and($binding->canonical_subject_key)->not->toContain('microsoft');
|
|
})->with([
|
|
'exact provider identity' => ['createExactProviderIdentity', ResourceIdentity::providerResource('fake-provider', 'policy', 'exact-1'), ProviderResourceResolutionMode::ExactProviderIdentity],
|
|
'provider default as canonical builtin' => ['createCanonicalBuiltin', ResourceIdentity::canonicalDefault('fake-provider', 'default', 'provider-default'), ProviderResourceResolutionMode::CanonicalBuiltin],
|
|
'canonical virtual target' => ['createCanonicalVirtualTarget', ResourceIdentity::virtualTarget('fake-provider', 'target', 'virtual-target'), ProviderResourceResolutionMode::CanonicalVirtualTarget],
|
|
'manual binding' => ['createManualBinding', ResourceIdentity::providerResource('fake-provider', 'policy', 'manual-1'), ProviderResourceResolutionMode::ManualBinding],
|
|
'excluded non governed' => ['createExclusion', ResourceIdentity::unsupported('fake-provider', 'object', 'excluded-1'), ProviderResourceResolutionMode::ExcludedNonGoverned],
|
|
'accepted limitation' => ['createAcceptedLimitation', ResourceIdentity::unsupported('fake-provider', 'object', 'limited-1'), ProviderResourceResolutionMode::AcceptedLimitation],
|
|
'unsupported coverage' => ['markUnsupported', ResourceIdentity::unsupported('fake-provider', 'object', 'unsupported-1'), ProviderResourceResolutionMode::UnsupportedCoverage],
|
|
'missing expected' => ['markMissingExpected', ResourceIdentity::unknown('fake-provider', 'object', 'missing-1'), ProviderResourceResolutionMode::MissingExpected],
|
|
]);
|