185 lines
7.6 KiB
PHP
185 lines
7.6 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 || ! $credential->expires_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,
|
|
);
|
|
}
|
|
}
|