Implements Spec 081 provider-connection cutover. Highlights: - Adds provider connection resolution + gating for operations/verification. - Adds provider credential observer wiring. - Updates Filament tenant verify flow to block with next-steps when provider connection isn’t ready. - Adds spec docs under specs/081-provider-connection-cutover/ and extensive Spec081 test coverage. Tests: - vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantSetupTest.php - Focused suites for ProviderConnections/Verification ran during implementation (see local logs). Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #98
146 lines
4.2 KiB
PHP
146 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Observers;
|
|
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Intune\AuditLogger;
|
|
|
|
class ProviderCredentialObserver
|
|
{
|
|
public function created(ProviderCredential $credential): void
|
|
{
|
|
$connection = $this->resolveConnection($credential);
|
|
|
|
if (! $connection instanceof ProviderConnection) {
|
|
return;
|
|
}
|
|
|
|
$tenant = $connection->tenant;
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return;
|
|
}
|
|
|
|
$this->audit(
|
|
tenant: $tenant,
|
|
connection: $connection,
|
|
action: 'provider_connection.credentials_created',
|
|
changedFields: ['type', 'client_id', 'client_secret'],
|
|
);
|
|
}
|
|
|
|
public function updated(ProviderCredential $credential): void
|
|
{
|
|
$connection = $this->resolveConnection($credential);
|
|
|
|
if (! $connection instanceof ProviderConnection) {
|
|
return;
|
|
}
|
|
|
|
$tenant = $connection->tenant;
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return;
|
|
}
|
|
|
|
$changedFields = $this->changedFields($credential);
|
|
|
|
if ($changedFields === []) {
|
|
return;
|
|
}
|
|
|
|
$action = in_array('client_secret', $changedFields, true)
|
|
? 'provider_connection.credentials_rotated'
|
|
: 'provider_connection.credentials_updated';
|
|
|
|
$this->audit(
|
|
tenant: $tenant,
|
|
connection: $connection,
|
|
action: $action,
|
|
changedFields: $changedFields,
|
|
);
|
|
}
|
|
|
|
private function resolveConnection(ProviderCredential $credential): ?ProviderConnection
|
|
{
|
|
$credential->loadMissing('providerConnection.tenant');
|
|
|
|
return $credential->providerConnection;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function changedFields(ProviderCredential $credential): array
|
|
{
|
|
$fields = [];
|
|
|
|
if ($credential->isDirty('type') || $credential->wasChanged('type')) {
|
|
$fields[] = 'type';
|
|
}
|
|
|
|
$previousPayload = $credential->getOriginal('payload');
|
|
$currentPayload = $credential->payload;
|
|
|
|
$previousPayload = is_array($previousPayload) ? $previousPayload : [];
|
|
$currentPayload = is_array($currentPayload) ? $currentPayload : [];
|
|
|
|
$previousClientId = trim((string) ($previousPayload['client_id'] ?? ''));
|
|
$currentClientId = trim((string) ($currentPayload['client_id'] ?? ''));
|
|
|
|
if ($previousClientId !== $currentClientId) {
|
|
$fields[] = 'client_id';
|
|
}
|
|
|
|
$previousClientSecret = trim((string) ($previousPayload['client_secret'] ?? ''));
|
|
$currentClientSecret = trim((string) ($currentPayload['client_secret'] ?? ''));
|
|
|
|
if ($previousClientSecret !== $currentClientSecret) {
|
|
$fields[] = 'client_secret';
|
|
}
|
|
|
|
return array_values(array_unique($fields));
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $changedFields
|
|
*/
|
|
private function audit(
|
|
Tenant $tenant,
|
|
ProviderConnection $connection,
|
|
string $action,
|
|
array $changedFields,
|
|
): void {
|
|
$user = auth()->user();
|
|
$actorId = $user instanceof User ? (int) $user->getKey() : null;
|
|
$actorEmail = $user instanceof User ? $user->email : null;
|
|
$actorName = $user instanceof User ? $user->name : null;
|
|
|
|
app(AuditLogger::class)->log(
|
|
tenant: $tenant,
|
|
action: $action,
|
|
context: [
|
|
'metadata' => [
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'provider' => (string) $connection->provider,
|
|
'entra_tenant_id' => (string) $connection->entra_tenant_id,
|
|
'credential_type' => (string) $connection->credential?->type,
|
|
'changed_fields' => $changedFields,
|
|
'redacted_fields' => ['client_secret'],
|
|
],
|
|
],
|
|
actorId: $actorId,
|
|
actorEmail: $actorEmail,
|
|
actorName: $actorName,
|
|
resourceType: 'provider_connection',
|
|
resourceId: (string) $connection->getKey(),
|
|
status: 'success',
|
|
);
|
|
}
|
|
}
|