## 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
118 lines
4.2 KiB
PHP
118 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Providers;
|
|
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Support\Providers\ProviderConnectionType;
|
|
use App\Support\Providers\ProviderCredentialKind;
|
|
use App\Support\Providers\ProviderCredentialSource;
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
|
|
final class CredentialManager
|
|
{
|
|
/**
|
|
* @return array{client_id: string, client_secret: string}
|
|
*/
|
|
public function getClientCredentials(ProviderConnection $connection): array
|
|
{
|
|
if ($connection->connection_type !== ProviderConnectionType::Dedicated) {
|
|
throw new InvalidArgumentException('Dedicated provider credentials are only available for dedicated connections.');
|
|
}
|
|
|
|
$credential = $connection->credential;
|
|
|
|
if (! $credential instanceof ProviderCredential) {
|
|
throw new RuntimeException('Provider credentials are missing.');
|
|
}
|
|
|
|
if ($credential->type !== 'client_secret') {
|
|
throw new RuntimeException('Unsupported provider credential type.');
|
|
}
|
|
|
|
$payload = $credential->payload;
|
|
|
|
if (! is_array($payload)) {
|
|
throw new RuntimeException('Provider credential payload is invalid.');
|
|
}
|
|
|
|
$clientId = trim((string) ($payload['client_id'] ?? ''));
|
|
$clientSecret = trim((string) ($payload['client_secret'] ?? ''));
|
|
|
|
if ($clientId === '' || $clientSecret === '') {
|
|
throw new RuntimeException('Provider credential payload is missing required keys.');
|
|
}
|
|
|
|
$tenantId = $payload['tenant_id'] ?? null;
|
|
|
|
if (is_string($tenantId) && $tenantId !== '' && $tenantId !== $connection->entra_tenant_id) {
|
|
throw new InvalidArgumentException('Provider credential tenant_id does not match the connection entra_tenant_id.');
|
|
}
|
|
|
|
return [
|
|
'client_id' => $clientId,
|
|
'client_secret' => $clientSecret,
|
|
];
|
|
}
|
|
|
|
public function upsertClientSecretCredential(
|
|
ProviderConnection $connection,
|
|
string $clientId,
|
|
string $clientSecret,
|
|
ProviderCredentialSource $source = ProviderCredentialSource::DedicatedManual,
|
|
): ProviderCredential {
|
|
$clientId = trim($clientId);
|
|
$clientSecret = trim($clientSecret);
|
|
|
|
if ($clientId === '' || $clientSecret === '') {
|
|
throw new InvalidArgumentException('client_id and client_secret are required.');
|
|
}
|
|
|
|
$existing = $connection->credential;
|
|
$existingPayload = $existing instanceof ProviderCredential && is_array($existing->payload)
|
|
? $existing->payload
|
|
: [];
|
|
$secretChanged = ! $existing instanceof ProviderCredential
|
|
|| trim((string) ($existingPayload['client_secret'] ?? '')) !== $clientSecret;
|
|
|
|
return ProviderCredential::query()->updateOrCreate(
|
|
[
|
|
'provider_connection_id' => $connection->getKey(),
|
|
],
|
|
[
|
|
'type' => 'client_secret',
|
|
'credential_kind' => ProviderCredentialKind::ClientSecret->value,
|
|
'source' => $source->value,
|
|
'last_rotated_at' => $secretChanged ? now() : $existing?->last_rotated_at,
|
|
'expires_at' => $existing?->expires_at,
|
|
'payload' => [
|
|
'client_id' => $clientId,
|
|
'client_secret' => $clientSecret,
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
public function updateClientIdPreservingSecret(ProviderConnection $connection, string $clientId): ProviderCredential
|
|
{
|
|
$clientId = trim($clientId);
|
|
|
|
if ($clientId === '') {
|
|
throw new InvalidArgumentException('client_id is required.');
|
|
}
|
|
|
|
$existing = $this->getClientCredentials($connection);
|
|
$source = $connection->credential?->source instanceof ProviderCredentialSource
|
|
? $connection->credential->source
|
|
: ProviderCredentialSource::DedicatedManual;
|
|
|
|
return $this->upsertClientSecretCredential(
|
|
connection: $connection,
|
|
clientId: $clientId,
|
|
clientSecret: (string) $existing['client_secret'],
|
|
source: $source,
|
|
);
|
|
}
|
|
}
|