TenantAtlas/apps/platform/app/Support/Providers/Readiness/ProviderReadinessResolver.php
Ahmed Darrazi 1245af12af
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m25s
feat: improve provider readiness semantics and freshness guidance
2026-06-21 19:18:00 +02:00

719 lines
27 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Providers\Readiness;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentPermission;
use App\Models\ProviderConnection;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Services\Intune\ManagedEnvironmentPermissionService;
use App\Services\Providers\ProviderConnectionResolver;
use App\Support\Auth\Capabilities;
use App\Support\Providers\ProviderConsentStatus;
use App\Support\Providers\ProviderReasonCodes;
use App\Support\Providers\ProviderVerificationStatus;
use Illuminate\Support\Carbon;
final class ProviderReadinessResolver
{
private const int FRESHNESS_DAYS = 30;
public function __construct(
private readonly ManagedEnvironmentPermissionService $permissionService,
private readonly ProviderConnectionResolver $connectionResolver,
private readonly CapabilityResolver $capabilityResolver,
) {}
public function forConnection(ProviderConnection $connection, ?User $actor = null): ProviderReadinessResult
{
$environment = $connection->tenant instanceof ManagedEnvironment
? $connection->tenant
: ManagedEnvironment::query()->whereKey((int) $connection->managed_environment_id)->first();
if (! $environment instanceof ManagedEnvironment) {
return $this->emptyResult(
provider: $this->provider($connection),
workspaceId: is_numeric($connection->workspace_id) ? (int) $connection->workspace_id : null,
environmentId: is_numeric($connection->managed_environment_id) ? (int) $connection->managed_environment_id : null,
connectionId: (int) $connection->getKey(),
state: ProviderReadinessState::Unknown,
actor: $actor,
reasonCode: ProviderReasonCodes::ProviderConnectionInvalid,
message: 'Provider readiness could not be resolved because the environment scope is unavailable.',
);
}
return $this->resolve(
environment: $environment,
connection: $connection,
actor: $actor,
provider: $this->provider($connection),
);
}
public function forEnvironment(
ManagedEnvironment $environment,
?User $actor = null,
string $provider = 'microsoft',
): ProviderReadinessResult {
$resolution = $this->connectionResolver->resolveDefault($environment, $provider);
return $this->resolve(
environment: $environment,
connection: $resolution->connection,
actor: $actor,
provider: $provider,
connectionResolutionReasonCode: $resolution->effectiveReasonCode(),
connectionResolutionMessage: $resolution->message,
);
}
public function forWorkspace(Workspace $workspace, ?User $actor = null, string $provider = 'microsoft'): ProviderReadinessResult
{
$children = ManagedEnvironment::query()
->where('workspace_id', (int) $workspace->getKey())
->orderBy('id')
->get()
->filter(fn (ManagedEnvironment $environment): bool => ! $actor instanceof User || $this->capabilityResolver->isMember($actor, $environment))
->map(fn (ManagedEnvironment $environment): ProviderReadinessResult => $this->forEnvironment($environment, $actor, $provider))
->values()
->all();
return $this->aggregate(
provider: $provider,
workspaceId: (int) $workspace->getKey(),
environmentId: null,
connectionId: null,
children: $children,
actor: $actor,
);
}
/**
* @param array<int, ProviderReadinessResult> $children
*/
private function aggregate(
string $provider,
?int $workspaceId,
?int $environmentId,
?int $connectionId,
array $children,
?User $actor,
): ProviderReadinessResult {
if ($children === []) {
return $this->emptyResult(
provider: $provider,
workspaceId: $workspaceId,
environmentId: $environmentId,
connectionId: $connectionId,
state: ProviderReadinessState::NotConfigured,
actor: $actor,
reasonCode: ProviderReasonCodes::ProviderConnectionMissing,
message: 'No provider readiness children are configured.',
);
}
$counts = $this->initialCounts();
$permissionRows = [];
$lastRefreshedAt = null;
foreach ($children as $child) {
foreach ($child->counts as $key => $value) {
if (! array_key_exists($key, $counts)) {
$counts[$key] = 0;
}
$counts[$key] += (int) $value;
}
$permissionRows = array_merge($permissionRows, $child->permissionRows);
$candidate = $this->parseTime($child->freshness['last_refreshed_at'] ?? null);
if ($candidate instanceof Carbon && (! $lastRefreshedAt instanceof Carbon || $candidate->gt($lastRefreshedAt))) {
$lastRefreshedAt = $candidate;
}
}
$state = $this->aggregateState($children);
return new ProviderReadinessResult(
provider: $provider,
workspaceId: $workspaceId,
managedEnvironmentId: $environmentId,
providerConnectionId: $connectionId,
state: $state,
counts: $this->finalizeCounts($counts),
permissionRows: $permissionRows,
freshness: $this->freshness($lastRefreshedAt),
canManageProvider: false,
canViewTechnicalDetail: false,
recommendedAction: $this->recommendedAction($state, false),
childResults: array_map(
static fn (ProviderReadinessResult $child): array => $child->toArray(),
$children,
),
technical: [],
);
}
private function resolve(
ManagedEnvironment $environment,
?ProviderConnection $connection,
?User $actor,
string $provider,
?string $connectionResolutionReasonCode = null,
?string $connectionResolutionMessage = null,
): ProviderReadinessResult {
$canManageProvider = $this->canManageProvider($actor, $environment);
$canViewTechnicalDetail = $this->canViewTechnicalDetail($actor, $environment);
$stored = $this->storedPermissions($environment);
$lastRefreshedAt = $this->latestStoredCheck($stored);
$healthCheckedAt = $this->parseTime($connection?->last_health_check_at);
$verificationState = $this->verificationState($connection);
$verificationNeverRun = $connection instanceof ProviderConnection && $healthCheckedAt === null;
$verificationStale = $healthCheckedAt instanceof Carbon && $healthCheckedAt->lt(now()->subDays(self::FRESHNESS_DAYS));
$rows = [];
foreach ($this->permissionService->getRequiredPermissions() as $permission) {
$key = (string) ($permission['key'] ?? '');
if ($key === '') {
continue;
}
$rows[] = $this->permissionRow(
permission: $permission,
stored: $stored[$key] ?? null,
connection: $connection,
verificationState: $verificationState,
verificationNeverRun: $verificationNeverRun,
verificationStale: $verificationStale,
);
}
$counts = $this->countsForRows($rows);
$state = $this->stateFor(
connection: $connection,
rows: $rows,
verificationState: $verificationState,
verificationNeverRun: $verificationNeverRun,
verificationStale: $verificationStale,
connectionResolutionReasonCode: $connectionResolutionReasonCode,
);
return new ProviderReadinessResult(
provider: $provider,
workspaceId: is_numeric($environment->workspace_id) ? (int) $environment->workspace_id : null,
managedEnvironmentId: (int) $environment->getKey(),
providerConnectionId: $connection instanceof ProviderConnection ? (int) $connection->getKey() : null,
state: $state,
counts: $counts,
permissionRows: $rows,
freshness: $this->freshness($lastRefreshedAt, $healthCheckedAt),
canManageProvider: $canManageProvider,
canViewTechnicalDetail: $canViewTechnicalDetail,
recommendedAction: $this->recommendedAction($state, $canManageProvider),
technical: [
'connection_resolution_reason_code' => $connectionResolutionReasonCode,
'connection_resolution_message' => $connectionResolutionMessage,
'verification_state' => $verificationState,
'verification_never_run' => $verificationNeverRun,
'verification_stale' => $verificationStale,
],
);
}
/**
* @param array<string, mixed> $permission
* @param array{status:string,details:array<string,mixed>|null,last_checked_at:?Carbon}|null $stored
* @return array<string, mixed>
*/
private function permissionRow(
array $permission,
?array $stored,
?ProviderConnection $connection,
string $verificationState,
bool $verificationNeverRun,
bool $verificationStale,
): array {
$storedStatus = is_string($stored['status'] ?? null) ? (string) $stored['status'] : null;
$details = is_array($stored['details'] ?? null) ? $stored['details'] : null;
$lastCheckedAt = $this->parseTime($stored['last_checked_at'] ?? null);
$storedStale = ! $lastCheckedAt instanceof Carbon || $lastCheckedAt->lt(now()->subDays(self::FRESHNESS_DAYS));
$scopeMatches = $connection instanceof ProviderConnection
&& $this->storedEvidenceMatchesConnection($details, $connection);
[$state, $reasonCode] = $this->permissionState(
storedStatus: $storedStatus,
connection: $connection,
verificationState: $verificationState,
verificationNeverRun: $verificationNeverRun,
verificationStale: $verificationStale,
storedStale: $storedStale,
scopeMatches: $scopeMatches,
details: $details,
);
return [
'key' => (string) ($permission['key'] ?? ''),
'type' => in_array(($permission['type'] ?? null), ['application', 'delegated'], true)
? (string) $permission['type']
: 'application',
'description' => is_string($permission['description'] ?? null) ? (string) $permission['description'] : null,
'features' => is_array($permission['features'] ?? null) ? array_values($permission['features']) : [],
'status' => $state->value,
'details' => $details,
'last_checked_at' => $lastCheckedAt?->toIso8601String(),
'provider_connection_id' => $connection instanceof ProviderConnection ? (int) $connection->getKey() : null,
'matched_grant_id' => $scopeMatches ? (int) ($details['provider_connection_id'] ?? 0) : null,
'is_effective' => $state === ProviderPermissionReadinessState::Granted,
'reason_code' => $reasonCode,
];
}
/**
* @param array<string, mixed>|null $details
* @return array{0: ProviderPermissionReadinessState, 1: string}
*/
private function permissionState(
?string $storedStatus,
?ProviderConnection $connection,
string $verificationState,
bool $verificationNeverRun,
bool $verificationStale,
bool $storedStale,
bool $scopeMatches,
?array $details,
): array {
if (! $connection instanceof ProviderConnection) {
return [ProviderPermissionReadinessState::Unknown, ProviderReasonCodes::ProviderConnectionMissing];
}
if ($storedStatus === 'granted' && ! $scopeMatches) {
return [ProviderPermissionReadinessState::Unknown, 'provider_permission_evidence_scope_mismatch'];
}
if ($verificationNeverRun) {
return [ProviderPermissionReadinessState::Unknown, 'provider_verification_not_run'];
}
if ($verificationStale || $storedStale) {
return [ProviderPermissionReadinessState::Expired, 'provider_permission_evidence_expired'];
}
if ($storedStatus === null) {
return $verificationState === ProviderVerificationStatus::Healthy->value
? [ProviderPermissionReadinessState::Missing, 'provider_permission_missing']
: [ProviderPermissionReadinessState::Unknown, 'provider_permission_evidence_unavailable'];
}
if ($storedStatus === 'granted') {
return [ProviderPermissionReadinessState::Granted, 'ok'];
}
if ($storedStatus === 'missing') {
return [ProviderPermissionReadinessState::Missing, 'provider_permission_missing'];
}
if ($storedStatus === 'error') {
$reasonCode = is_string($details['reason_code'] ?? null) ? (string) $details['reason_code'] : 'provider_permission_refresh_failed';
return in_array($reasonCode, ['authentication_failed', 'permission_denied'], true)
? [ProviderPermissionReadinessState::Blocked, $reasonCode]
: [ProviderPermissionReadinessState::Unknown, $reasonCode];
}
return [ProviderPermissionReadinessState::Unknown, 'provider_permission_state_unknown'];
}
private function stateFor(
?ProviderConnection $connection,
array $rows,
string $verificationState,
bool $verificationNeverRun,
bool $verificationStale,
?string $connectionResolutionReasonCode,
): ProviderReadinessState {
if (! $connection instanceof ProviderConnection) {
return ProviderReadinessState::NotConfigured;
}
if (! (bool) $connection->is_enabled) {
return ProviderReadinessState::NotConfigured;
}
if ($connectionResolutionReasonCode !== null && $connectionResolutionReasonCode !== 'unknown_error') {
return $this->connectionResolutionState($connectionResolutionReasonCode);
}
$consentStatus = $this->consentState($connection);
if ($consentStatus !== ProviderConsentStatus::Granted->value) {
return ProviderReadinessState::Blocked;
}
if ($verificationState === ProviderVerificationStatus::Error->value || $verificationState === ProviderVerificationStatus::Degraded->value) {
return ProviderReadinessState::Failed;
}
if ($verificationState === ProviderVerificationStatus::Blocked->value) {
return ProviderReadinessState::Blocked;
}
if ($verificationNeverRun || $verificationState === ProviderVerificationStatus::Pending->value || $verificationState === ProviderVerificationStatus::Unknown->value) {
return ProviderReadinessState::Unknown;
}
if ($verificationStale || $this->containsState($rows, ProviderPermissionReadinessState::Expired)) {
return ProviderReadinessState::Expired;
}
if ($this->containsState($rows, ProviderPermissionReadinessState::Blocked)) {
return ProviderReadinessState::Blocked;
}
if ($this->containsState($rows, ProviderPermissionReadinessState::Missing)) {
return ProviderReadinessState::NeedsAttention;
}
if ($this->containsState($rows, ProviderPermissionReadinessState::Unknown)) {
return ProviderReadinessState::Unknown;
}
return ProviderReadinessState::Ready;
}
private function connectionResolutionState(string $reasonCode): ProviderReadinessState
{
return match ($reasonCode) {
ProviderReasonCodes::ProviderConnectionMissing => ProviderReadinessState::NotConfigured,
ProviderReasonCodes::ProviderConsentMissing,
ProviderReasonCodes::ProviderConsentFailed,
ProviderReasonCodes::ProviderConsentRevoked,
ProviderReasonCodes::TenantTargetMismatch,
ProviderReasonCodes::ProviderConnectionInvalid,
ProviderReasonCodes::ProviderConnectionReviewRequired,
ProviderReasonCodes::ProviderBindingUnsupported => ProviderReadinessState::Blocked,
default => ProviderReadinessState::Unknown,
};
}
/**
* @param array<int, array<string, mixed>> $rows
*/
private function containsState(array $rows, ProviderPermissionReadinessState $state): bool
{
return collect($rows)->contains(
static fn (array $row): bool => ($row['status'] ?? null) === $state->value,
);
}
/**
* @param array<int, ProviderReadinessResult> $children
*/
private function aggregateState(array $children): ProviderReadinessState
{
$states = array_map(static fn (ProviderReadinessResult $result): ProviderReadinessState => $result->state, $children);
foreach ([
ProviderReadinessState::NotConfigured,
ProviderReadinessState::Failed,
ProviderReadinessState::Blocked,
ProviderReadinessState::Expired,
ProviderReadinessState::NeedsAttention,
ProviderReadinessState::Unknown,
] as $state) {
if (in_array($state, $states, true)) {
return $state;
}
}
return ProviderReadinessState::Ready;
}
/**
* @param array<string, array{status:string,details:array<string,mixed>|null,last_checked_at:?Carbon}> $stored
*/
private function latestStoredCheck(array $stored): ?Carbon
{
$latest = null;
foreach ($stored as $row) {
$candidate = $this->parseTime($row['last_checked_at'] ?? null);
if ($candidate instanceof Carbon && (! $latest instanceof Carbon || $candidate->gt($latest))) {
$latest = $candidate;
}
}
return $latest;
}
/**
* @return array<string, array{status:string,details:array<string,mixed>|null,last_checked_at:?Carbon}>
*/
private function storedPermissions(ManagedEnvironment $environment): array
{
$query = ManagedEnvironmentPermission::query()
->where('managed_environment_id', (int) $environment->getKey());
if (is_numeric($environment->workspace_id)) {
$query->where('workspace_id', (int) $environment->workspace_id);
}
return $query
->get()
->keyBy('permission_key')
->map(static fn (ManagedEnvironmentPermission $permission): array => [
'status' => (string) $permission->status,
'details' => is_array($permission->details) ? $permission->details : null,
'last_checked_at' => $permission->last_checked_at,
])
->all();
}
/**
* @param array<string, mixed>|null $details
*/
private function storedEvidenceMatchesConnection(?array $details, ProviderConnection $connection): bool
{
if (! is_array($details)) {
return false;
}
$providerConnectionId = $details['provider_connection_id'] ?? null;
if (! is_numeric($providerConnectionId) || (int) $providerConnectionId !== (int) $connection->getKey()) {
return false;
}
$workspaceId = $details['workspace_id'] ?? null;
if (! is_numeric($workspaceId) || ! is_numeric($connection->workspace_id) || (int) $workspaceId !== (int) $connection->workspace_id) {
return false;
}
$environmentId = $details['managed_environment_id'] ?? null;
if (! is_numeric($environmentId) || ! is_numeric($connection->managed_environment_id) || (int) $environmentId !== (int) $connection->managed_environment_id) {
return false;
}
$provider = $details['provider'] ?? null;
return is_string($provider) && trim($provider) !== '' && trim($provider) === $this->provider($connection);
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<string, int>
*/
private function countsForRows(array $rows): array
{
$counts = $this->initialCounts();
foreach ($rows as $row) {
$status = (string) ($row['status'] ?? ProviderPermissionReadinessState::Unknown->value);
if (! array_key_exists($status, $counts)) {
$counts[$status] = 0;
}
$counts[$status] += 1;
if ($status === ProviderPermissionReadinessState::Missing->value) {
if (($row['type'] ?? null) === 'delegated') {
$counts['missing_delegated'] += 1;
} else {
$counts['missing_application'] += 1;
}
}
}
return $this->finalizeCounts($counts);
}
/**
* @return array<string, int>
*/
private function initialCounts(): array
{
return [
'required' => 0,
'granted' => 0,
'missing' => 0,
'missing_application' => 0,
'missing_delegated' => 0,
'blocked' => 0,
'expired' => 0,
'unknown' => 0,
'not_applicable' => 0,
];
}
/**
* @param array<string, int> $counts
* @return array<string, int>
*/
private function finalizeCounts(array $counts): array
{
$counts['required'] = (int) ($counts['granted'] ?? 0)
+ (int) ($counts['missing'] ?? 0)
+ (int) ($counts['blocked'] ?? 0)
+ (int) ($counts['expired'] ?? 0)
+ (int) ($counts['unknown'] ?? 0);
$counts['error'] = (int) ($counts['blocked'] ?? 0) + (int) ($counts['unknown'] ?? 0);
return $counts;
}
/**
* @return array<string, mixed>
*/
private function freshness(?Carbon $lastRefreshedAt, ?Carbon $healthCheckedAt = null): array
{
$permissionEvidenceStale = ! $lastRefreshedAt instanceof Carbon
|| $lastRefreshedAt->lt(now()->subDays(self::FRESHNESS_DAYS));
$verificationEvidenceStale = ! $healthCheckedAt instanceof Carbon
|| $healthCheckedAt->lt(now()->subDays(self::FRESHNESS_DAYS));
return [
'last_refreshed_at' => $lastRefreshedAt?->toIso8601String(),
'last_health_check_at' => $healthCheckedAt?->toIso8601String(),
'is_stale' => $permissionEvidenceStale || $verificationEvidenceStale,
'stale_after_days' => self::FRESHNESS_DAYS,
];
}
/**
* @return array<string, mixed>
*/
private function recommendedAction(ProviderReadinessState $state, bool $canManageProvider): array
{
[$label, $actionName] = match ($state) {
ProviderReadinessState::Ready => ['View provider connection', 'viewProviderConnection'],
ProviderReadinessState::NeedsAttention => ['Review required permissions', 'reviewRequiredPermissions'],
ProviderReadinessState::Blocked => ['Resolve provider blocker', 'manageProviderConnection'],
ProviderReadinessState::NotConfigured => ['Connect provider', 'manageProviderConnection'],
ProviderReadinessState::Expired => ['Run provider verification', 'runProviderVerification'],
ProviderReadinessState::Failed => ['Review provider error', 'manageProviderConnection'],
ProviderReadinessState::Unknown => ['Check provider status', 'runProviderVerification'],
};
return [
'label' => $label,
'action_name' => $actionName,
'disabled' => ! $canManageProvider && $state !== ProviderReadinessState::Ready,
];
}
private function emptyResult(
string $provider,
?int $workspaceId,
?int $environmentId,
?int $connectionId,
ProviderReadinessState $state,
?User $actor,
?string $reasonCode,
?string $message,
): ProviderReadinessResult {
$counts = $this->finalizeCounts($this->initialCounts());
return new ProviderReadinessResult(
provider: $provider,
workspaceId: $workspaceId,
managedEnvironmentId: $environmentId,
providerConnectionId: $connectionId,
state: $state,
counts: $counts,
permissionRows: [],
freshness: $this->freshness(null),
canManageProvider: false,
canViewTechnicalDetail: false,
recommendedAction: $this->recommendedAction($state, false),
technical: [
'reason_code' => $reasonCode,
'message' => $message,
],
);
}
private function canManageProvider(?User $actor, ManagedEnvironment $environment): bool
{
if (! $actor instanceof User) {
return false;
}
return $this->capabilityResolver->isMember($actor, $environment)
&& (
$this->capabilityResolver->can($actor, $environment, Capabilities::PROVIDER_MANAGE)
|| $this->capabilityResolver->can($actor, $environment, Capabilities::PROVIDER_RUN)
);
}
private function canViewTechnicalDetail(?User $actor, ManagedEnvironment $environment): bool
{
if (! $actor instanceof User) {
return false;
}
return $this->capabilityResolver->isMember($actor, $environment)
&& (
$this->capabilityResolver->can($actor, $environment, Capabilities::PROVIDER_MANAGE)
|| $this->capabilityResolver->can($actor, $environment, Capabilities::SUPPORT_DIAGNOSTICS_VIEW)
);
}
private function provider(ProviderConnection $connection): string
{
$provider = trim((string) $connection->provider);
return $provider !== '' ? $provider : 'microsoft';
}
private function verificationState(?ProviderConnection $connection): string
{
$state = $connection?->verification_status;
if ($state instanceof ProviderVerificationStatus) {
return $state->value;
}
return is_string($state) && trim($state) !== ''
? trim($state)
: ProviderVerificationStatus::Unknown->value;
}
private function consentState(ProviderConnection $connection): string
{
$state = $connection->consent_status;
if ($state instanceof ProviderConsentStatus) {
return $state->value;
}
return is_string($state) && trim($state) !== ''
? trim($state)
: ProviderConsentStatus::Unknown->value;
}
private function parseTime(mixed $value): ?Carbon
{
if ($value instanceof Carbon) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return Carbon::instance($value);
}
if (is_string($value) && trim($value) !== '') {
try {
return Carbon::parse($value);
} catch (\Throwable) {
return null;
}
}
return null;
}
}