TenantAtlas/apps/platform/tests/Feature/ProviderResources/ProviderResourceBindingServiceTest.php
Ahmed Darrazi fb2642e941
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m9s
feat(resources): implement provider resource identity binding
Added ProviderResourceBinding model, migrations, policies, and supporting framework for canonical resource identity mapping as defined in Spec 381.
2026-06-15 17:37:06 +02:00

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],
]);