TenantAtlas/apps/platform/app/Services/TenantConfiguration/CoverageEvidenceWriter.php
ahmido a23131cdbc feat: add Exchange evidence capture adapter guard (#501)
Summary: add Spec 434 Exchange PowerShell evidence capture adapter and prerequisite/identity/content-only guards; cap Exchange evidence at content_backed while preserving Graph capture. Validation: php artisan test --filter=Spec434 --compact; ./vendor/bin/pint --dirty --test; git diff --cached --check. Product Surface: N/A - no rendered UI surface changed; no deployment impact.
Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #501
2026-07-08 10:46:05 +00:00

182 lines
7.1 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,
private readonly ExchangeTeamsRenderableSummaryBuilder $exchangeTeamsSummaryBuilder,
private readonly SecurityComplianceRenderableSummaryBuilder $securityComplianceSummaryBuilder,
) {}
/**
* @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 = [],
?CoverageLevel $maximumCoverageLevel = null,
): 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,
$maximumCoverageLevel,
): TenantConfigurationResourceEvidence {
$capturedAt = now();
$coverageLevel = $this->cappedCoverageLevel(
$this->coverageLevelFor($resourceType, $normalizedPayload),
$maximumCoverageLevel,
);
$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;
}
private function cappedCoverageLevel(CoverageLevel $coverageLevel, ?CoverageLevel $maximumCoverageLevel): CoverageLevel
{
if (! $maximumCoverageLevel instanceof CoverageLevel) {
return $coverageLevel;
}
return $this->coverageLevelRank($coverageLevel) <= $this->coverageLevelRank($maximumCoverageLevel)
? $coverageLevel
: $maximumCoverageLevel;
}
private function coverageLevelRank(CoverageLevel $coverageLevel): int
{
return match ($coverageLevel) {
CoverageLevel::Detected => 10,
CoverageLevel::ContentBacked => 20,
CoverageLevel::Comparable => 30,
CoverageLevel::Renderable => 40,
CoverageLevel::Restorable => 50,
CoverageLevel::Certified => 60,
};
}
/**
* @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;
}
if ($this->exchangeTeamsSummaryBuilder->canBuild($canonicalType, $normalizedPayload)) {
return CoverageLevel::Renderable;
}
if ($this->securityComplianceSummaryBuilder->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;
}
}