TenantAtlas/app/Observers/ProviderCredentialObserver.php
ahmido 4db8030f2a Spec 081: Provider connection cutover (#98)
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
2026-02-08 11:28:51 +00:00

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',
);
}
}