Replaced legacy tenant and environment bindings in the BaselineDriftEngine with the new ProviderResourceIdentity framework as defined in Spec 382. This ensures cross-environment compatibility and deterministic baseline matching. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #453
221 lines
11 KiB
PHP
221 lines
11 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\BaselineSubjectKey;
|
|
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('rejects arbitrary canonical subject key overrides for provider resource decisions', function (): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
$identity = ResourceIdentity::providerResource('fake-provider', 'policy', 'canonical-validation');
|
|
$expectedCanonicalKey = BaselineSubjectKey::forProviderResourceIdentity(
|
|
subjectDomain: 'baseline',
|
|
subjectClass: SubjectClass::PolicyBacked,
|
|
subjectTypeKey: 'deviceConfiguration',
|
|
identity: $identity,
|
|
);
|
|
|
|
expect(fn () => app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: $identity,
|
|
attributes: providerResourceBindingAttributes([
|
|
'canonical_subject_key' => 'duplicate policy',
|
|
]),
|
|
))->toThrow(InvalidArgumentException::class, 'canonical subject keys');
|
|
|
|
$binding = app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: $identity,
|
|
attributes: providerResourceBindingAttributes([
|
|
'canonical_subject_key' => $expectedCanonicalKey,
|
|
]),
|
|
);
|
|
|
|
expect($binding->canonical_subject_key)->toBe($expectedCanonicalKey);
|
|
});
|
|
|
|
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],
|
|
]);
|