provider) !== 'microsoft') { return $this->blocked( failureCode: 'permission_evidence_blocked_unsupported_provider', stateKey: 'permission_evidence_state', state: 'unsupported_provider', context: ['provider' => 'unsupported'], ); } $permission = ManagedEnvironmentPermission::query() ->where('workspace_id', (int) $environment->workspace_id) ->where('managed_environment_id', (int) $environment->getKey()) ->where('permission_key', self::PERMISSION_KEY) ->orderByDesc('last_checked_at') ->orderByDesc('id') ->first(); if (! $permission instanceof ManagedEnvironmentPermission) { return $this->blocked('permission_evidence_blocked_missing', 'permission_evidence_state', 'missing'); } $details = is_array($permission->details) ? $permission->details : []; $status = $this->normalizedStatus($permission->status); if ($status !== 'granted') { return match ($status) { 'missing', 'absent', 'revoked' => $this->blocked('permission_evidence_blocked_absent', 'permission_evidence_state', 'absent', $permission), 'unknown', 'error' => $this->blocked('permission_evidence_blocked_unknown', 'permission_evidence_state', 'unknown', $permission), default => $this->blocked('permission_evidence_blocked_unvalidated', 'permission_evidence_state', 'unvalidated', $permission), }; } $metadata = $this->validatedEvidenceMetadata($details); if ($metadata === null) { return $this->blocked('permission_evidence_blocked_unvalidated', 'permission_evidence_state', 'unvalidated', $permission); } if ($this->permissionEvidenceIsStale($permission, $metadata)) { return $this->blocked('permission_evidence_blocked_stale', 'permission_evidence_state', 'stale', $permission); } $scopeIds = $this->validatedScopeIds($details); if ($scopeIds === null) { return $this->blocked('permission_evidence_blocked_unvalidated', 'permission_evidence_state', 'unvalidated', $permission); } if ($scopeIds['workspace_id'] !== (int) $connection->workspace_id) { return $this->blocked('permission_evidence_blocked_wrong_workspace', 'permission_evidence_state', 'wrong_workspace', $permission); } if ($scopeIds['managed_environment_id'] !== (int) $connection->managed_environment_id) { return $this->blocked('permission_evidence_blocked_wrong_environment', 'permission_evidence_state', 'wrong_environment', $permission); } if (trim((string) ($details['provider'] ?? '')) !== 'microsoft' || trim((string) ($details['provider'] ?? '')) !== trim((string) $connection->provider) ) { return $this->blocked('permission_evidence_blocked_unsupported_provider', 'permission_evidence_state', 'unsupported_provider', $permission); } if ($scopeIds['provider_connection_id'] !== (int) $connection->getKey()) { return $this->blocked('permission_evidence_blocked_wrong_provider_connection', 'permission_evidence_state', 'wrong_provider_connection', $permission); } return ExchangePowerShellGateResult::allowed([ 'permission_evidence_state' => 'verified', 'permission_key' => self::PERMISSION_KEY, 'permission_evidence_id' => (int) $permission->getKey(), 'last_checked_at' => $permission->last_checked_at?->toJSON(), 'permission_currentness_days' => $this->currentnessDays(), 'permission_evidence_source' => $metadata['source'], 'permission_observed_at' => $metadata['observed_at'], 'permission_verified_at' => $metadata['verified_at'], 'permission_evidence_evaluator' => $metadata['evaluator'], 'permission_evidence_evaluator_version' => $metadata['evaluator_version'], ]); } private function currentnessDays(): int { $configured = config('tenantpilot.exchange_powershell.permission_evidence.currentness_days', 30); return max(1, (int) $configured); } private function normalizedStatus(mixed $status): string { if (! is_string($status) || trim($status) === '') { return 'unknown'; } $status = strtolower(trim($status)); return in_array($status, ['granted', 'missing', 'absent', 'revoked', 'blocked', 'unknown', 'error'], true) ? $status : 'unknown'; } /** * @param array{source: string, observed_at: string, verified_at: string, evaluator: string, evaluator_version: string} $metadata */ private function permissionEvidenceIsStale(ManagedEnvironmentPermission $permission, array $metadata): bool { $freshSince = now()->subDays($this->currentnessDays()); $observedAt = $this->safeTimestamp($metadata['observed_at']); $verifiedAt = $this->safeTimestamp($metadata['verified_at']); return ! $permission->last_checked_at || $permission->last_checked_at->lt($freshSince) || ! $observedAt instanceof Carbon || ! $verifiedAt instanceof Carbon || $observedAt->lt($freshSince) || $verifiedAt->lt($freshSince); } /** * @param array $details * @return array{source: string, observed_at: string, verified_at: string, evaluator: string, evaluator_version: string}|null */ private function validatedEvidenceMetadata(array $details): ?array { $source = $this->safeString($details['source'] ?? null); if ($source !== self::TRUSTED_EVIDENCE_SOURCE) { return null; } $observedAt = $this->safeTimestamp($details['observed_at'] ?? null); $verifiedAt = $this->safeTimestamp($details['verified_at'] ?? null); if (! $observedAt instanceof Carbon || ! $verifiedAt instanceof Carbon) { return null; } if ($observedAt->isFuture() || $verifiedAt->isFuture() || $verifiedAt->lt($observedAt)) { return null; } $evaluator = $this->safeString($details['evaluator'] ?? null); $evaluatorVersion = $this->safeString($details['evaluator_version'] ?? null); if ($evaluator !== self::EVIDENCE_EVALUATOR || $evaluatorVersion !== self::EVIDENCE_EVALUATOR_VERSION) { return null; } return [ 'source' => $source, 'observed_at' => $observedAt->toJSON(), 'verified_at' => $verifiedAt->toJSON(), 'evaluator' => $evaluator, 'evaluator_version' => $evaluatorVersion, ]; } private function safeString(mixed $value): ?string { if (! is_string($value)) { return null; } $value = trim($value); if ($value === '') { return null; } return preg_match('/\A[A-Za-z0-9_.:-]{1,80}\z/', $value) === 1 ? $value : null; } private function safeTimestamp(mixed $value): ?Carbon { if (! is_string($value)) { return null; } $value = trim($value); if ($value === '') { return null; } $matches = []; if (preg_match('/\A(?\d{4})-(?0[1-9]|1[0-2])-(?0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,6})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)\z/', $value, $matches) !== 1) { return null; } if (! checkdate((int) $matches['month'], (int) $matches['day'], (int) $matches['year'])) { return null; } try { return Carbon::parse($value); } catch (Throwable) { return null; } } /** * @param array $details * @return array{workspace_id: int, managed_environment_id: int, provider_connection_id: int}|null */ private function validatedScopeIds(array $details): ?array { $workspaceId = $this->safePositiveInteger($details['workspace_id'] ?? null); $environmentId = $this->safePositiveInteger($details['managed_environment_id'] ?? null); $providerConnectionId = $this->safePositiveInteger($details['provider_connection_id'] ?? null); if ($workspaceId === null || $environmentId === null || $providerConnectionId === null) { return null; } return [ 'workspace_id' => $workspaceId, 'managed_environment_id' => $environmentId, 'provider_connection_id' => $providerConnectionId, ]; } private function safePositiveInteger(mixed $value): ?int { if (is_int($value)) { return $value > 0 ? $value : null; } if (! is_string($value)) { return null; } if ($value === '' || $value !== trim($value) || preg_match('/\A[1-9]\d{0,18}\z/', $value) !== 1) { return null; } $integer = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); return is_int($integer) ? $integer : null; } private function blocked( string $failureCode, string $stateKey, string $state, ?ManagedEnvironmentPermission $permission = null, array $context = [], ): ExchangePowerShellGateResult { return ExchangePowerShellGateResult::blocked( reasonCode: ProviderReasonCodes::ProviderPermissionMissing, failureCode: $failureCode, message: 'Verified Exchange PowerShell permission evidence is required.', context: array_filter([ $stateKey => $state, 'permission_key' => self::PERMISSION_KEY, 'permission_evidence_id' => $permission instanceof ManagedEnvironmentPermission ? (int) $permission->getKey() : null, 'permission_currentness_days' => $this->currentnessDays(), ...$context, ], static fn (mixed $value): bool => $value !== null && $value !== ''), ); } }