## Summary - standardize Microsoft provider connections around explicit platform vs dedicated identity modes - centralize admin-consent URL and runtime identity resolution so platform flows no longer fall back to tenant-local credentials - add migration classification, richer consent and verification state handling, dedicated override management, and focused regression coverage ## Validation - focused repo test coverage was added across provider identity, onboarding, audit, policy, guard, and migration flows - latest explicit passing run in the workspace: `vendor/bin/sail artisan test --compact tests/Feature/AdminConsentCallbackTest.php tests/Feature/Audit/ProviderConnectionConsentAuditTest.php` ## Notes - branch includes the full Spec 137 artifact set under `specs/137-platform-provider-identity/` - target base branch: `dev` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #166
184 lines
7.4 KiB
PHP
184 lines
7.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Providers;
|
|
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Support\Providers\ProviderConnectionType;
|
|
use App\Support\Providers\ProviderConsentStatus;
|
|
use App\Support\Providers\ProviderReasonCodes;
|
|
use App\Support\Providers\ProviderVerificationStatus;
|
|
use Illuminate\Support\Facades\DB;
|
|
use InvalidArgumentException;
|
|
|
|
final class ProviderConnectionMutationService
|
|
{
|
|
public function __construct(
|
|
private readonly CredentialManager $credentials,
|
|
private readonly ProviderConnectionStateProjector $stateProjector,
|
|
) {}
|
|
|
|
public function enableDedicatedOverride(
|
|
ProviderConnection $connection,
|
|
string $clientId,
|
|
string $clientSecret,
|
|
): ProviderConnection {
|
|
$clientId = trim($clientId);
|
|
$clientSecret = trim($clientSecret);
|
|
|
|
if ($clientId === '' || $clientSecret === '') {
|
|
throw new InvalidArgumentException('Dedicated client_id and client_secret are required.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($connection, $clientId, $clientSecret): ProviderConnection {
|
|
$connection->loadMissing('credential');
|
|
|
|
$credential = $connection->credential;
|
|
$payload = $credential instanceof ProviderCredential && is_array($credential->payload)
|
|
? $credential->payload
|
|
: [];
|
|
|
|
$previousClientId = trim((string) ($payload['client_id'] ?? ''));
|
|
$needsConsentReset = $connection->connection_type !== ProviderConnectionType::Dedicated
|
|
|| $previousClientId === ''
|
|
|| $previousClientId !== $clientId;
|
|
|
|
$consentStatus = $needsConsentReset
|
|
? ProviderConsentStatus::Required
|
|
: $this->normalizeConsentStatus($connection->consent_status);
|
|
$verificationStatus = ProviderVerificationStatus::Unknown;
|
|
|
|
$updates = $this->projectConnectionState(
|
|
connection: $connection,
|
|
connectionType: ProviderConnectionType::Dedicated,
|
|
consentStatus: $consentStatus,
|
|
verificationStatus: $verificationStatus,
|
|
);
|
|
|
|
$connection->forceFill(array_merge($updates, [
|
|
'connection_type' => ProviderConnectionType::Dedicated->value,
|
|
'last_health_check_at' => null,
|
|
'last_error_reason_code' => $needsConsentReset
|
|
? ProviderReasonCodes::ProviderConsentMissing
|
|
: null,
|
|
'last_error_message' => null,
|
|
'scopes_granted' => $needsConsentReset ? [] : ($connection->scopes_granted ?? []),
|
|
'consent_granted_at' => $needsConsentReset ? null : $connection->consent_granted_at,
|
|
'consent_last_checked_at' => $needsConsentReset ? null : $connection->consent_last_checked_at,
|
|
'consent_error_code' => $needsConsentReset ? null : $connection->consent_error_code,
|
|
'consent_error_message' => $needsConsentReset ? null : $connection->consent_error_message,
|
|
]))->save();
|
|
|
|
$this->credentials->upsertClientSecretCredential(
|
|
connection: $connection->fresh(),
|
|
clientId: $clientId,
|
|
clientSecret: $clientSecret,
|
|
);
|
|
|
|
return $connection->fresh(['credential']);
|
|
});
|
|
}
|
|
|
|
public function revertToPlatform(ProviderConnection $connection): ProviderConnection
|
|
{
|
|
return DB::transaction(function () use ($connection): ProviderConnection {
|
|
$connection->loadMissing('credential');
|
|
|
|
if ($connection->credential instanceof ProviderCredential) {
|
|
$connection->credential->delete();
|
|
}
|
|
|
|
$updates = $this->projectConnectionState(
|
|
connection: $connection,
|
|
connectionType: ProviderConnectionType::Platform,
|
|
consentStatus: ProviderConsentStatus::Required,
|
|
verificationStatus: ProviderVerificationStatus::Unknown,
|
|
);
|
|
|
|
$connection->forceFill(array_merge($updates, [
|
|
'connection_type' => ProviderConnectionType::Platform->value,
|
|
'consent_granted_at' => null,
|
|
'consent_last_checked_at' => null,
|
|
'consent_error_code' => null,
|
|
'consent_error_message' => null,
|
|
'verification_status' => ProviderVerificationStatus::Unknown->value,
|
|
'last_health_check_at' => null,
|
|
'last_error_reason_code' => ProviderReasonCodes::ProviderConsentMissing,
|
|
'last_error_message' => null,
|
|
'scopes_granted' => [],
|
|
]))->save();
|
|
|
|
return $connection->fresh(['credential']);
|
|
});
|
|
}
|
|
|
|
public function deleteDedicatedCredential(ProviderConnection $connection): ProviderConnection
|
|
{
|
|
return DB::transaction(function () use ($connection): ProviderConnection {
|
|
$connection->loadMissing('credential');
|
|
|
|
if ($connection->credential instanceof ProviderCredential) {
|
|
$connection->credential->delete();
|
|
}
|
|
|
|
$consentStatus = $this->normalizeConsentStatus($connection->consent_status);
|
|
$verificationStatus = ProviderVerificationStatus::Blocked;
|
|
|
|
$updates = $this->projectConnectionState(
|
|
connection: $connection,
|
|
connectionType: ProviderConnectionType::Dedicated,
|
|
consentStatus: $consentStatus,
|
|
verificationStatus: $verificationStatus,
|
|
);
|
|
|
|
$connection->forceFill(array_merge($updates, [
|
|
'connection_type' => ProviderConnectionType::Dedicated->value,
|
|
'consent_status' => $consentStatus->value,
|
|
'verification_status' => $verificationStatus->value,
|
|
'last_health_check_at' => null,
|
|
'last_error_reason_code' => ProviderReasonCodes::DedicatedCredentialMissing,
|
|
'last_error_message' => 'Dedicated credential is missing.',
|
|
]))->save();
|
|
|
|
return $connection->fresh(['credential']);
|
|
});
|
|
}
|
|
|
|
private function projectConnectionState(
|
|
ProviderConnection $connection,
|
|
ProviderConnectionType $connectionType,
|
|
ProviderConsentStatus $consentStatus,
|
|
ProviderVerificationStatus $verificationStatus,
|
|
): array {
|
|
$projected = $this->stateProjector->project(
|
|
connectionType: $connectionType,
|
|
consentStatus: $consentStatus,
|
|
verificationStatus: $verificationStatus,
|
|
currentStatus: is_string($connection->status) ? $connection->status : null,
|
|
);
|
|
|
|
return [
|
|
'consent_status' => $consentStatus->value,
|
|
'verification_status' => $verificationStatus->value,
|
|
'status' => $projected['status'],
|
|
'health_status' => $projected['health_status'],
|
|
];
|
|
}
|
|
|
|
private function normalizeConsentStatus(
|
|
ProviderConsentStatus|string|null $consentStatus,
|
|
): ProviderConsentStatus {
|
|
if ($consentStatus instanceof ProviderConsentStatus) {
|
|
return $consentStatus;
|
|
}
|
|
|
|
if (is_string($consentStatus)) {
|
|
return ProviderConsentStatus::tryFrom(trim($consentStatus)) ?? ProviderConsentStatus::Required;
|
|
}
|
|
|
|
return ProviderConsentStatus::Required;
|
|
}
|
|
}
|