Automated PR created by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #465
719 lines
27 KiB
PHP
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;
|
|
}
|
|
}
|