TenantAtlas/apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCredentialReferenceResolver.php
Ahmed Darrazi 90a411429e
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m23s
feat: add Exchange PowerShell production runner gate
2026-07-07 20:32:36 +02:00

185 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
use App\Support\Providers\ProviderCredentialKind;
use App\Support\Providers\ProviderReasonCodes;
use Illuminate\Support\Carbon;
final class ExchangePowerShellCredentialReferenceResolver
{
public function resolve(ProviderConnection $connection): ExchangePowerShellGateResult
{
$credential = ProviderCredential::query()
->where('provider_connection_id', (int) $connection->getKey())
->first();
if (! $credential instanceof ProviderCredential) {
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialMissing,
failureCode: 'credential_blocked_missing_reference',
message: 'Exchange PowerShell requires a provider credential reference.',
context: ['credential_state' => 'missing'],
);
}
$kind = $this->credentialKind($credential);
$baseContext = [
'credential_state' => 'blocked',
'credential_kind' => $this->safeCredentialKindLabel($kind),
'credential_reference_id' => (int) $credential->getKey(),
'credential_material_persisted' => false,
];
if ($kind === ProviderCredentialKind::ClientSecret->value) {
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialInvalid,
failureCode: 'credential_blocked_secret_based_app',
message: 'Secret-based app credentials are not supported for Exchange PowerShell production invocation.',
context: $this->credentialContext($baseContext, 'secret_based_app_unsupported'),
);
}
if ($kind === ProviderCredentialKind::Certificate->value) {
if (! $credential->last_rotated_at instanceof Carbon) {
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialMissing,
failureCode: 'credential_blocked_certificate_missing',
message: 'Certificate credential metadata is incomplete.',
context: $this->credentialContext($baseContext, 'certificate_missing'),
);
}
if ($credential->expires_at instanceof Carbon && $credential->expires_at->isPast()) {
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialInvalid,
failureCode: 'credential_blocked_certificate_expired',
message: 'Certificate credential reference is expired.',
context: $this->credentialContext($baseContext, 'certificate_expired'),
);
}
if ($this->credentialKindSupported($kind)) {
return ExchangePowerShellGateResult::allowed($this->credentialContext($baseContext, 'certificate_supported'));
}
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialInvalid,
failureCode: 'credential_blocked_certificate_unsupported',
message: 'Certificate credentials are not enabled for Exchange PowerShell production invocation.',
context: $this->credentialContext($baseContext, 'certificate_unsupported'),
);
}
if ($kind === ProviderCredentialKind::Federated->value) {
if (! $credential->last_rotated_at instanceof Carbon) {
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialMissing,
failureCode: 'credential_blocked_federated_missing',
message: 'Federated credential metadata is incomplete.',
context: $this->credentialContext($baseContext, 'federated_missing'),
);
}
if ($this->credentialKindSupported($kind)) {
return ExchangePowerShellGateResult::allowed($this->credentialContext($baseContext, 'federated_supported'));
}
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialInvalid,
failureCode: 'credential_blocked_federated_unsupported',
message: 'Federated credentials are not enabled for Exchange PowerShell production invocation.',
context: $this->credentialContext($baseContext, 'federated_unsupported'),
);
}
if ($kind === 'managed_identity') {
if ($this->credentialKindSupported($kind)) {
return ExchangePowerShellGateResult::allowed($this->credentialContext($baseContext, 'managed_identity_supported'));
}
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialInvalid,
failureCode: 'credential_blocked_managed_identity_unsupported',
message: 'Managed identity credentials are not enabled for Exchange PowerShell production invocation.',
context: $this->credentialContext($baseContext, 'managed_identity_unsupported'),
);
}
return $this->blocked(
reasonCode: ProviderReasonCodes::ProviderCredentialInvalid,
failureCode: 'credential_blocked_unknown_kind',
message: 'Provider credential kind is not supported for Exchange PowerShell production invocation.',
context: $this->credentialContext($baseContext, 'unknown_kind'),
);
}
private function credentialKind(ProviderCredential $credential): string
{
$kind = $credential->getRawOriginal('credential_kind');
if (! is_string($kind) || trim($kind) === '') {
$kind = $credential->getRawOriginal('type');
}
return is_string($kind) ? strtolower(trim($kind)) : '';
}
private function credentialKindSupported(string $kind): bool
{
$supportedKinds = config('tenantpilot.exchange_powershell.credentials.supported_reference_kinds', []);
if (is_string($supportedKinds)) {
$supportedKinds = explode(',', $supportedKinds);
}
if (! is_array($supportedKinds)) {
return false;
}
$supportedKinds = array_map(
static fn (mixed $supportedKind): string => strtolower(trim((string) $supportedKind)),
$supportedKinds,
);
return in_array($kind, $supportedKinds, true);
}
private function safeCredentialKindLabel(string $kind): string
{
return match ($kind) {
ProviderCredentialKind::ClientSecret->value => 'secret_based_app',
ProviderCredentialKind::Certificate->value => 'certificate',
ProviderCredentialKind::Federated->value => 'federated',
'managed_identity' => 'managed_identity',
default => 'unknown',
};
}
/**
* @param array<string, mixed> $baseContext
* @return array<string, mixed>
*/
private function credentialContext(array $baseContext, string $state): array
{
return array_replace($baseContext, ['credential_state' => $state]);
}
/**
* @param array<string, mixed> $context
*/
private function blocked(string $reasonCode, string $failureCode, string $message, array $context): ExchangePowerShellGateResult
{
return ExchangePowerShellGateResult::blocked(
reasonCode: $reasonCode,
failureCode: $failureCode,
message: $message,
context: $context,
);
}
}