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 $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 $permission * @param array{status:string,details:array|null,last_checked_at:?Carbon}|null $stored * @return array */ 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|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> $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 $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|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|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|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> $rows * @return array */ 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 */ 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 $counts * @return array */ 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 */ 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 */ 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; } }