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
156 lines
6.4 KiB
PHP
156 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\BaselineTenantAssignment;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderResourceBinding;
|
|
use App\Services\Resources\ProviderResourceBindingService;
|
|
use App\Support\Baselines\SubjectClass;
|
|
use App\Support\Resources\ResourceIdentity;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Support\Facades\Gate;
|
|
|
|
function providerResourceBindingAuthorizationAttributes(array $overrides = []): array
|
|
{
|
|
return array_replace([
|
|
'subject_domain' => 'baseline',
|
|
'subject_class' => SubjectClass::PolicyBacked,
|
|
'subject_type_key' => 'deviceConfiguration',
|
|
'operator_note' => 'Authorization test decision note.',
|
|
], $overrides);
|
|
}
|
|
|
|
it('allows managers to create provider resource bindings', function (): void {
|
|
[$manager, $tenant] = createUserWithTenant(role: 'manager');
|
|
|
|
$binding = app(ProviderResourceBindingService::class)->createExactProviderIdentity(
|
|
actor: $manager,
|
|
environment: $tenant,
|
|
identity: ResourceIdentity::providerResource('fake-provider', 'policy', 'manager-allowed'),
|
|
attributes: providerResourceBindingAuthorizationAttributes(),
|
|
);
|
|
|
|
expect($binding->exists)->toBeTrue();
|
|
});
|
|
|
|
it('returns forbidden for entitled readonly users missing baseline manage capability', function (): void {
|
|
[$readonly, $tenant] = createUserWithTenant(role: 'readonly');
|
|
|
|
try {
|
|
app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $readonly,
|
|
environment: $tenant,
|
|
identity: ResourceIdentity::providerResource('fake-provider', 'policy', 'readonly-denied'),
|
|
attributes: providerResourceBindingAuthorizationAttributes(),
|
|
);
|
|
|
|
$this->fail('Expected authorization denial.');
|
|
} catch (AuthorizationException $exception) {
|
|
expect($exception->status())->toBe(403);
|
|
}
|
|
});
|
|
|
|
it('returns deny-as-not-found for users outside the binding workspace', function (): void {
|
|
[$actor] = createUserWithTenant(role: 'owner');
|
|
[, $foreignTenant] = createUserWithTenant(role: 'owner');
|
|
|
|
try {
|
|
app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $actor,
|
|
environment: $foreignTenant,
|
|
identity: ResourceIdentity::providerResource('fake-provider', 'policy', 'foreign-denied'),
|
|
attributes: providerResourceBindingAuthorizationAttributes(),
|
|
);
|
|
|
|
$this->fail('Expected not-found authorization denial.');
|
|
} catch (AuthorizationException $exception) {
|
|
expect($exception->status())->toBe(404);
|
|
}
|
|
});
|
|
|
|
it('rejects provider connections from another managed environment as not found', function (): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
[, $foreignTenant] = createUserWithTenant(role: 'owner');
|
|
$foreignConnection = ProviderConnection::factory()->create([
|
|
'workspace_id' => (int) $foreignTenant->workspace_id,
|
|
'managed_environment_id' => (int) $foreignTenant->getKey(),
|
|
'provider' => 'fake-provider',
|
|
]);
|
|
|
|
expect(fn () => app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: ResourceIdentity::providerResource('fake-provider', 'policy', 'foreign-connection'),
|
|
attributes: providerResourceBindingAuthorizationAttributes([
|
|
'provider_connection_id' => (int) $foreignConnection->getKey(),
|
|
]),
|
|
))->toThrow(ModelNotFoundException::class);
|
|
});
|
|
|
|
it('rejects source references from another managed environment as not found', function (): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
[, $foreignTenant] = createUserWithTenant(role: 'owner');
|
|
$foreignInventory = InventoryItem::factory()->create([
|
|
'managed_environment_id' => (int) $foreignTenant->getKey(),
|
|
]);
|
|
|
|
expect(fn () => app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: ResourceIdentity::providerResource('fake-provider', 'policy', 'foreign-source'),
|
|
attributes: providerResourceBindingAuthorizationAttributes([
|
|
'source_inventory_item_id' => (int) $foreignInventory->getKey(),
|
|
]),
|
|
))->toThrow(ModelNotFoundException::class);
|
|
});
|
|
|
|
it('rejects baseline snapshot references not assigned to the binding managed environment as not found', function (): void {
|
|
[$actor, $tenant] = createUserWithTenant(role: 'owner');
|
|
$foreignTenant = ManagedEnvironment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
$foreignProfile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$foreignSnapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $foreignProfile->getKey(),
|
|
]);
|
|
|
|
BaselineTenantAssignment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $foreignTenant->getKey(),
|
|
'baseline_profile_id' => (int) $foreignProfile->getKey(),
|
|
]);
|
|
|
|
expect(fn () => app(ProviderResourceBindingService::class)->createManualBinding(
|
|
actor: $actor,
|
|
environment: $tenant,
|
|
identity: ResourceIdentity::providerResource('fake-provider', 'policy', 'foreign-baseline-snapshot'),
|
|
attributes: providerResourceBindingAuthorizationAttributes([
|
|
'source_baseline_snapshot_id' => (int) $foreignSnapshot->getKey(),
|
|
]),
|
|
))->toThrow(ModelNotFoundException::class);
|
|
});
|
|
|
|
it('uses baseline capabilities in the registered policy', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
[$readonly] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
|
|
|
$binding = ProviderResourceBinding::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
]);
|
|
|
|
expect(Gate::forUser($owner)->inspect('revoke', $binding)->allowed())->toBeTrue()
|
|
->and(Gate::forUser($readonly)->inspect('view', $binding)->allowed())->toBeTrue()
|
|
->and(Gate::forUser($readonly)->inspect('revoke', $binding)->status())->toBe(403);
|
|
});
|