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
215 lines
6.8 KiB
PHP
215 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Resources;
|
|
|
|
use Illuminate\Contracts\Support\Arrayable;
|
|
use InvalidArgumentException;
|
|
use JsonSerializable;
|
|
|
|
/**
|
|
* @implements Arrayable<string, mixed>
|
|
*/
|
|
final readonly class ResourceIdentity implements Arrayable, JsonSerializable
|
|
{
|
|
public const string ProviderResource = 'provider_resource';
|
|
|
|
public const string CanonicalBuiltin = 'canonical_builtin';
|
|
|
|
public const string CanonicalDefault = 'canonical_default';
|
|
|
|
public const string CanonicalVirtualTarget = 'canonical_virtual_target';
|
|
|
|
public const string Unsupported = 'unsupported';
|
|
|
|
public const string Unknown = 'unknown';
|
|
|
|
public function __construct(
|
|
public string $providerKey,
|
|
public string $identityKind,
|
|
public ?string $providerResourceType = null,
|
|
public ?string $providerResourceId = null,
|
|
public ?string $canonicalDiscriminator = null,
|
|
) {
|
|
$this->assertValid();
|
|
}
|
|
|
|
public static function providerResource(string $providerKey, string $resourceType, string $resourceId): self
|
|
{
|
|
return new self(
|
|
providerKey: $providerKey,
|
|
identityKind: self::ProviderResource,
|
|
providerResourceType: $resourceType,
|
|
providerResourceId: $resourceId,
|
|
);
|
|
}
|
|
|
|
public static function canonicalBuiltin(string $providerKey, string $resourceType, string $discriminator): self
|
|
{
|
|
return new self(
|
|
providerKey: $providerKey,
|
|
identityKind: self::CanonicalBuiltin,
|
|
providerResourceType: $resourceType,
|
|
canonicalDiscriminator: $discriminator,
|
|
);
|
|
}
|
|
|
|
public static function canonicalDefault(string $providerKey, string $resourceType, string $discriminator): self
|
|
{
|
|
return new self(
|
|
providerKey: $providerKey,
|
|
identityKind: self::CanonicalDefault,
|
|
providerResourceType: $resourceType,
|
|
canonicalDiscriminator: $discriminator,
|
|
);
|
|
}
|
|
|
|
public static function virtualTarget(string $providerKey, string $resourceType, string $discriminator): self
|
|
{
|
|
return new self(
|
|
providerKey: $providerKey,
|
|
identityKind: self::CanonicalVirtualTarget,
|
|
providerResourceType: $resourceType,
|
|
canonicalDiscriminator: $discriminator,
|
|
);
|
|
}
|
|
|
|
public static function unsupported(string $providerKey, string $resourceType, string $discriminator): self
|
|
{
|
|
return new self(
|
|
providerKey: $providerKey,
|
|
identityKind: self::Unsupported,
|
|
providerResourceType: $resourceType,
|
|
canonicalDiscriminator: $discriminator,
|
|
);
|
|
}
|
|
|
|
public static function unknown(string $providerKey, string $resourceType, string $discriminator): self
|
|
{
|
|
return new self(
|
|
providerKey: $providerKey,
|
|
identityKind: self::Unknown,
|
|
providerResourceType: $resourceType,
|
|
canonicalDiscriminator: $discriminator,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
public static function fromArray(array $payload): self
|
|
{
|
|
return new self(
|
|
providerKey: (string) ($payload['provider_key'] ?? ''),
|
|
identityKind: (string) ($payload['identity_kind'] ?? ''),
|
|
providerResourceType: self::nullableString($payload['provider_resource_type'] ?? null),
|
|
providerResourceId: self::nullableString($payload['provider_resource_id'] ?? null),
|
|
canonicalDiscriminator: self::nullableString($payload['canonical_discriminator'] ?? null),
|
|
);
|
|
}
|
|
|
|
public function stableIdentityValue(): ?string
|
|
{
|
|
return $this->identityKind === self::ProviderResource
|
|
? $this->providerResourceId
|
|
: $this->canonicalDiscriminator;
|
|
}
|
|
|
|
public function fingerprint(): string
|
|
{
|
|
return hash('sha256', json_encode($this->payload(), JSON_THROW_ON_ERROR));
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* provider_key: string,
|
|
* identity_kind: string,
|
|
* provider_resource_type: ?string,
|
|
* provider_resource_id: ?string,
|
|
* canonical_discriminator: ?string,
|
|
* fingerprint: string
|
|
* }
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
$payload = $this->payload();
|
|
|
|
return $payload + [
|
|
'fingerprint' => hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* provider_key: string,
|
|
* identity_kind: string,
|
|
* provider_resource_type: ?string,
|
|
* provider_resource_id: ?string,
|
|
* canonical_discriminator: ?string
|
|
* }
|
|
*/
|
|
private function payload(): array
|
|
{
|
|
return [
|
|
'provider_key' => $this->providerKey,
|
|
'identity_kind' => $this->identityKind,
|
|
'provider_resource_type' => $this->providerResourceType,
|
|
'provider_resource_id' => $this->providerResourceId,
|
|
'canonical_discriminator' => $this->canonicalDiscriminator,
|
|
];
|
|
}
|
|
|
|
public function jsonSerialize(): array
|
|
{
|
|
return $this->toArray();
|
|
}
|
|
|
|
private function assertValid(): void
|
|
{
|
|
if (trim($this->providerKey) === '') {
|
|
throw new InvalidArgumentException('Resource identities require a provider key.');
|
|
}
|
|
|
|
if (! in_array($this->identityKind, self::validKinds(), true)) {
|
|
throw new InvalidArgumentException(sprintf('Unsupported resource identity kind [%s].', $this->identityKind));
|
|
}
|
|
|
|
if ($this->providerResourceType !== null && trim($this->providerResourceType) === '') {
|
|
throw new InvalidArgumentException('Provider resource type must be non-empty when supplied.');
|
|
}
|
|
|
|
if ($this->identityKind === self::ProviderResource) {
|
|
if ($this->providerResourceType === null || trim($this->providerResourceType) === '' || $this->providerResourceId === null || trim($this->providerResourceId) === '') {
|
|
throw new InvalidArgumentException('Provider resource identities require provider resource type and ID.');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if ($this->canonicalDiscriminator === null || trim($this->canonicalDiscriminator) === '') {
|
|
throw new InvalidArgumentException('Canonical resource identities require a discriminator.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function validKinds(): array
|
|
{
|
|
return [
|
|
self::ProviderResource,
|
|
self::CanonicalBuiltin,
|
|
self::CanonicalDefault,
|
|
self::CanonicalVirtualTarget,
|
|
self::Unsupported,
|
|
self::Unknown,
|
|
];
|
|
}
|
|
|
|
private static function nullableString(mixed $value): ?string
|
|
{
|
|
return is_string($value) && trim($value) !== '' ? trim($value) : null;
|
|
}
|
|
}
|