Implements the bounded Spec 421 Entra comparable/renderable pack on the existing Coverage v2 operator surface. - Adds typed Conditional Access normalization, comparison, and render summaries - Keeps Security Defaults and other optional Entra types deferred until evidence-backed - Preserves the existing Coverage v2 surface with claim-guard and redaction hardening - Includes focused unit, feature, and browser coverage already recorded in the implementation report Validation is documented in `specs/421-entra-core-comparable-renderable-pack/implementation-report.md`. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #488
144 lines
5.6 KiB
PHP
144 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\TenantConfiguration;
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\TenantConfigurationResource;
|
|
use App\Models\TenantConfigurationResourceEvidence;
|
|
use App\Models\TenantConfigurationResourceType;
|
|
use App\Support\TenantConfiguration\CaptureOutcome;
|
|
use App\Support\TenantConfiguration\CoverageLevel;
|
|
use App\Support\TenantConfiguration\EvidenceState;
|
|
use BackedEnum;
|
|
use Illuminate\Support\Facades\DB;
|
|
use InvalidArgumentException;
|
|
|
|
final class CoverageEvidenceWriter
|
|
{
|
|
public function __construct(
|
|
private readonly EntraRenderableSummaryBuilder $entraSummaryBuilder,
|
|
) {}
|
|
|
|
/**
|
|
* @param array<string, mixed> $rawPayload
|
|
* @param array<string, mixed> $normalizedPayload
|
|
* @param array<string, mixed> $permissionContext
|
|
*/
|
|
public function append(
|
|
TenantConfigurationResource $resource,
|
|
TenantConfigurationResourceType $resourceType,
|
|
ProviderConnection $providerConnection,
|
|
OperationRun $operationRun,
|
|
CoverageSourceContractDecision $decision,
|
|
array $rawPayload,
|
|
array $normalizedPayload,
|
|
string $payloadHash,
|
|
array $permissionContext = [],
|
|
): TenantConfigurationResourceEvidence {
|
|
if (! $decision->capturable()) {
|
|
throw new InvalidArgumentException('Cannot append captured evidence for a non-capturable source contract decision.');
|
|
}
|
|
|
|
$this->assertScoped($resource, $resourceType, $providerConnection, $operationRun);
|
|
|
|
/** @var TenantConfigurationResourceEvidence $evidence */
|
|
$evidence = DB::transaction(function () use (
|
|
$resource,
|
|
$resourceType,
|
|
$providerConnection,
|
|
$operationRun,
|
|
$decision,
|
|
$rawPayload,
|
|
$normalizedPayload,
|
|
$payloadHash,
|
|
$permissionContext,
|
|
): TenantConfigurationResourceEvidence {
|
|
$capturedAt = now();
|
|
|
|
$coverageLevel = $this->coverageLevelFor($resourceType, $normalizedPayload);
|
|
|
|
$evidence = TenantConfigurationResourceEvidence::query()->create([
|
|
'resource_id' => (int) $resource->getKey(),
|
|
'workspace_id' => (int) $resource->workspace_id,
|
|
'managed_environment_id' => (int) $resource->managed_environment_id,
|
|
'provider_connection_id' => (int) $providerConnection->getKey(),
|
|
'resource_type_id' => (int) $resourceType->getKey(),
|
|
'operation_run_id' => (int) $operationRun->getKey(),
|
|
'source_contract_key' => (string) $decision->contractKey,
|
|
'source_endpoint' => (string) $decision->sourceEndpoint,
|
|
'source_version' => $decision->sourceVersion,
|
|
'source_schema_hash' => $decision->sourceSchemaHash,
|
|
'source_metadata' => $decision->sourceMetadata,
|
|
'raw_payload' => $rawPayload,
|
|
'normalized_payload' => $normalizedPayload,
|
|
'payload_hash' => $payloadHash,
|
|
'permission_context' => $permissionContext === [] ? (object) [] : $permissionContext,
|
|
'evidence_state' => EvidenceState::ContentBacked->value,
|
|
'coverage_level' => $coverageLevel->value,
|
|
'capture_outcome' => CaptureOutcome::Captured->value,
|
|
'captured_at' => $capturedAt,
|
|
]);
|
|
|
|
$resource->forceFill([
|
|
'latest_evidence_id' => (int) $evidence->getKey(),
|
|
'latest_evidence_state' => EvidenceState::ContentBacked->value,
|
|
'latest_identity_state' => $this->stringValue($resource->latest_identity_state),
|
|
'latest_claim_state' => $this->stringValue($resource->latest_claim_state),
|
|
'latest_payload_hash' => $payloadHash,
|
|
'latest_captured_at' => $capturedAt,
|
|
])->save();
|
|
|
|
return $evidence;
|
|
});
|
|
|
|
return $evidence;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $normalizedPayload
|
|
*/
|
|
private function coverageLevelFor(TenantConfigurationResourceType $resourceType, array $normalizedPayload): CoverageLevel
|
|
{
|
|
$canonicalType = (string) $resourceType->canonical_type;
|
|
|
|
if ($this->entraSummaryBuilder->canBuild($canonicalType, $normalizedPayload)) {
|
|
return CoverageLevel::Renderable;
|
|
}
|
|
|
|
return CoverageLevel::ContentBacked;
|
|
}
|
|
|
|
private function assertScoped(
|
|
TenantConfigurationResource $resource,
|
|
TenantConfigurationResourceType $resourceType,
|
|
ProviderConnection $providerConnection,
|
|
OperationRun $operationRun,
|
|
): void {
|
|
if ((int) $resource->resource_type_id !== (int) $resourceType->getKey()) {
|
|
throw new InvalidArgumentException('Resource type mismatch while appending tenant configuration evidence.');
|
|
}
|
|
|
|
if ((int) $resource->provider_connection_id !== (int) $providerConnection->getKey()) {
|
|
throw new InvalidArgumentException('Provider connection mismatch while appending tenant configuration evidence.');
|
|
}
|
|
|
|
if ((int) $operationRun->workspace_id !== (int) $resource->workspace_id
|
|
|| (int) $operationRun->managed_environment_id !== (int) $resource->managed_environment_id
|
|
) {
|
|
throw new InvalidArgumentException('Operation run scope mismatch while appending tenant configuration evidence.');
|
|
}
|
|
}
|
|
|
|
private function stringValue(mixed $value): string
|
|
{
|
|
if ($value instanceof BackedEnum) {
|
|
return (string) $value->value;
|
|
}
|
|
|
|
return (string) $value;
|
|
}
|
|
}
|