Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m9s
Added ProviderResourceBinding model, migrations, policies, and supporting framework for canonical resource identity mapping as defined in Spec 381.
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],
|
|
]);
|