TenantAtlas/app/Filament/Resources/ProviderConnectionResource/Pages/EditProviderConnection.php
ahmido 92f39d9749 feat: add shared reason translation contract (#187)
## Summary
- introduce a shared reason-translation contract with envelopes, presenter helpers, fallback handling, and provider translation support
- adopt translated operator-facing reason presentation across operation runs, notifications, provider guidance, tenant operability, and RBAC-related surfaces
- add Spec 157 design artifacts and targeted regression coverage for translation quality, diagnostics retention, and authorization-safe guidance

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Architecture/ReasonTranslationPrimarySurfaceGuardTest.php tests/Unit/Support/ReasonTranslation/ReasonResolutionEnvelopeTest.php tests/Unit/Support/ReasonTranslation/ExecutionDenialReasonTranslationTest.php tests/Unit/Support/ReasonTranslation/TenantOperabilityReasonTranslationTest.php tests/Unit/Support/ReasonTranslation/RbacReasonTranslationTest.php tests/Unit/Support/ReasonTranslation/ProviderReasonTranslationTest.php tests/Feature/Notifications/OperationRunNotificationTest.php tests/Feature/Operations/OperationRunBlockedExecutionPresentationTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/ReasonTranslation/GovernanceReasonPresentationTest.php tests/Feature/Authorization/ReasonTranslationScopeSafetyTest.php tests/Feature/Monitoring/OperationRunBlockedSpec081Test.php tests/Feature/ProviderConnections/ProviderOperationBlockedGuidanceSpec081Test.php tests/Feature/ProviderConnections/ProviderGatewayRuntimeSmokeSpec081Test.php`

## Notes
- Livewire v4.0+ compliance remains unchanged within the existing Filament v5 stack.
- No new panel was added; provider registration remains in `bootstrap/providers.php`.
- No new globally searchable resource was introduced.
- No new destructive action family was introduced.
- No new assets were added; the existing `filament:assets` deployment behavior remains unchanged.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #187
2026-03-22 20:19:43 +00:00

1020 lines
46 KiB
PHP

<?php
namespace App\Filament\Resources\ProviderConnectionResource\Pages;
use App\Filament\Resources\ProviderConnectionResource;
use App\Jobs\ProviderComplianceSnapshotJob;
use App\Jobs\ProviderInventorySyncJob;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Services\Intune\AuditLogger;
use App\Services\Providers\ProviderConnectionMutationService;
use App\Services\Providers\ProviderOperationStartGate;
use App\Services\Verification\StartVerification;
use App\Support\Auth\Capabilities;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\OpsUx\OpsUxBrowserEvents;
use App\Support\Providers\ProviderConnectionType;
use App\Support\Rbac\UiEnforcement;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
class EditProviderConnection extends EditRecord
{
protected static string $resource = ProviderConnectionResource::class;
public ?string $scopedTenantExternalId = null;
protected bool $shouldMakeDefault = false;
protected bool $defaultWasChanged = false;
public function mount($record): void
{
parent::mount($record);
$recordTenant = $this->record instanceof ProviderConnection
? ProviderConnectionResource::resolveTenantForRecord($this->record)
: null;
if ($recordTenant instanceof Tenant) {
$this->scopedTenantExternalId = (string) $recordTenant->external_id;
return;
}
$tenantIdFromQuery = request()->query('tenant_id');
if (is_string($tenantIdFromQuery) && $tenantIdFromQuery !== '') {
$this->scopedTenantExternalId = $tenantIdFromQuery;
return;
}
$tenant = request()->route('tenant');
if ($tenant instanceof Tenant) {
$this->scopedTenantExternalId = (string) $tenant->external_id;
return;
}
if (is_string($tenant) && $tenant !== '') {
$this->scopedTenantExternalId = $tenant;
}
}
protected function mutateFormDataBeforeSave(array $data): array
{
$this->shouldMakeDefault = (bool) ($data['is_default'] ?? false);
unset($data['is_default']);
return $data;
}
protected function afterSave(): void
{
$record = $this->getRecord();
$tenant = $record instanceof ProviderConnection
? ($record->tenant ?? $this->currentTenant())
: $this->currentTenant();
if (! $tenant instanceof Tenant) {
return;
}
$changedFields = array_values(array_diff(array_keys($record->getChanges()), ['updated_at']));
if ($this->shouldMakeDefault && ! $record->is_default) {
$record->makeDefault();
$this->defaultWasChanged = true;
}
$hasDefault = $tenant->providerConnections()
->where('provider', $record->provider)
->where('is_default', true)
->exists();
if (! $hasDefault) {
$record->makeDefault();
$this->defaultWasChanged = true;
}
$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;
if ($changedFields !== []) {
app(AuditLogger::class)->log(
tenant: $tenant,
action: 'provider_connection.updated',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
'fields' => $changedFields,
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
}
if ($this->defaultWasChanged) {
app(AuditLogger::class)->log(
tenant: $tenant,
action: 'provider_connection.default_set',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
}
}
protected function getHeaderActions(): array
{
$tenant = $this->currentTenant();
return [
Actions\DeleteAction::make()
->visible(false),
Actions\ActionGroup::make([
UiEnforcement::forAction(
Action::make('view_last_check_run')
->label('View last check run')
->icon('heroicon-o-eye')
->color('gray')
->visible(fn (ProviderConnection $record): bool => $tenant instanceof Tenant
&& OperationRun::query()
->where('tenant_id', $tenant->getKey())
->where('type', 'provider.connection.check')
->where('context->provider_connection_id', (int) $record->getKey())
->exists())
->url(function (ProviderConnection $record): ?string {
$tenant = $this->currentTenant();
if (! $tenant instanceof Tenant) {
return null;
}
$run = OperationRun::query()
->where('tenant_id', $tenant->getKey())
->where('type', 'provider.connection.check')
->where('context->provider_connection_id', (int) $record->getKey())
->orderByDesc('id')
->first();
if (! $run instanceof OperationRun) {
return null;
}
return OperationRunLinks::view($run, $tenant);
})
)
->requireCapability(Capabilities::PROVIDER_VIEW)
->tooltip('You do not have permission to view provider connections.')
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('check_connection')
->label('Check connection')
->icon('heroicon-o-check-badge')
->color('success')
->visible(function (ProviderConnection $record): bool {
$tenant = $this->currentTenant();
$user = auth()->user();
return $tenant instanceof Tenant
&& $user instanceof User
&& $user->canAccessTenant($tenant)
&& $record->status !== 'disabled';
})
->action(function (ProviderConnection $record, StartVerification $verification): void {
$tenant = $this->currentTenant();
$user = auth()->user();
if (! $tenant instanceof Tenant) {
abort(404);
}
if (! $user instanceof User) {
abort(403);
}
if (! $user->canAccessTenant($tenant)) {
abort(404);
}
$initiator = $user;
$result = $verification->providerConnectionCheck(
tenant: $tenant,
connection: $record,
initiator: $initiator,
);
if ($result->status === 'scope_busy') {
Notification::make()
->title('Scope busy')
->body('Another provider operation is already running for this connection.')
->warning()
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
Action::make('manage_connections')
->label('Manage Provider Connections')
->url(ProviderConnectionResource::getUrl('index', tenant: $tenant)),
])
->send();
return;
}
if ($result->status === 'deduped') {
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
Action::make('manage_connections')
->label('Manage Provider Connections')
->url(ProviderConnectionResource::getUrl('index', tenant: $tenant)),
])
->send();
return;
}
if ($result->status === 'blocked') {
$reasonCode = is_string($result->run->context['reason_code'] ?? null)
? (string) $result->run->context['reason_code']
: 'unknown_error';
$reasonEnvelope = app(\App\Support\ReasonTranslation\ReasonPresenter::class)->forOperationRun($result->run, 'notification');
$bodyLines = $reasonEnvelope?->toBodyLines() ?? ['Blocked by provider configuration.'];
Notification::make()
->title('Connection check blocked')
->body(implode("\n", $bodyLines))
->warning()
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
Action::make('manage_connections')
->label('Manage Provider Connections')
->url(ProviderConnectionResource::getUrl('index', tenant: $tenant)),
])
->send();
return;
}
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
})
)
->preserveVisibility()
->requireCapability(Capabilities::PROVIDER_RUN)
->apply(),
UiEnforcement::forAction(
Action::make('enable_dedicated_override')
->label('Enable dedicated override')
->icon('heroicon-o-key')
->color('primary')
->requiresConfirmation()
->modalDescription('Dedicated credentials are stored encrypted and reset consent to the dedicated app registration.')
->visible(fn (ProviderConnection $record): bool => $tenant instanceof Tenant
&& $record->connection_type !== ProviderConnectionType::Dedicated)
->form([
TextInput::make('client_id')
->label('Dedicated app (client) ID')
->required()
->maxLength(255),
TextInput::make('client_secret')
->label('Dedicated client secret')
->password()
->required()
->maxLength(255),
])
->action(function (array $data, ProviderConnection $record, ProviderConnectionMutationService $mutations, AuditLogger $auditLogger): void {
$tenant = $this->currentTenant();
if (! $tenant instanceof Tenant) {
abort(404);
}
$mutations->enableDedicatedOverride(
connection: $record,
clientId: (string) $data['client_id'],
clientSecret: (string) $data['client_secret'],
);
$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;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.connection_type_changed',
context: [
'metadata' => [
'provider_connection_id' => (int) $record->getKey(),
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
'from_connection_type' => ProviderConnectionType::Platform->value,
'to_connection_type' => ProviderConnectionType::Dedicated->value,
'client_id' => (string) $data['client_id'],
'source' => 'provider_connection.edit_page',
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
Notification::make()
->title('Dedicated override enabled')
->success()
->send();
})
)
->requireCapability(Capabilities::PROVIDER_MANAGE_DEDICATED)
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('rotate_dedicated_credential')
->label('Rotate dedicated credential')
->icon('heroicon-o-arrow-path')
->color('primary')
->requiresConfirmation()
->modalDescription('Stores a replacement dedicated client secret and refreshes dedicated identity state.')
->visible(fn (ProviderConnection $record): bool => $tenant instanceof Tenant
&& $record->connection_type === ProviderConnectionType::Dedicated)
->form([
TextInput::make('client_id')
->label('Dedicated app (client) ID')
->default(function (ProviderConnection $record): string {
$payload = $record->credential?->payload;
return is_array($payload) ? (string) ($payload['client_id'] ?? '') : '';
})
->required()
->maxLength(255),
TextInput::make('client_secret')
->label('Dedicated client secret')
->password()
->required()
->maxLength(255),
])
->action(function (array $data, ProviderConnection $record, ProviderConnectionMutationService $mutations): void {
$tenant = $this->currentTenant();
if (! $tenant instanceof Tenant) {
abort(404);
}
$mutations->enableDedicatedOverride(
connection: $record,
clientId: (string) $data['client_id'],
clientSecret: (string) $data['client_secret'],
);
Notification::make()
->title('Dedicated credential rotated')
->success()
->send();
})
)
->requireCapability(Capabilities::PROVIDER_MANAGE_DEDICATED)
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('delete_dedicated_credential')
->label('Delete dedicated credential')
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->modalDescription('Deletes the dedicated credential and leaves the connection blocked until a replacement is added or the type is reverted.')
->visible(fn (ProviderConnection $record): bool => $tenant instanceof Tenant
&& $record->connection_type === ProviderConnectionType::Dedicated
&& $record->credential()->exists())
->action(function (ProviderConnection $record, ProviderConnectionMutationService $mutations): void {
$tenant = $this->currentTenant();
if (! $tenant instanceof Tenant) {
abort(404);
}
$mutations->deleteDedicatedCredential($record);
Notification::make()
->title('Dedicated credential deleted')
->warning()
->send();
})
)
->requireCapability(Capabilities::PROVIDER_MANAGE_DEDICATED)
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('revert_to_platform')
->label('Revert to platform')
->icon('heroicon-o-arrow-uturn-left')
->color('gray')
->requiresConfirmation()
->modalDescription('Reverts the connection to the platform-managed identity and removes any dedicated credential.')
->visible(fn (ProviderConnection $record): bool => $tenant instanceof Tenant
&& $record->connection_type === ProviderConnectionType::Dedicated)
->action(function (ProviderConnection $record, ProviderConnectionMutationService $mutations, AuditLogger $auditLogger): void {
$tenant = $this->currentTenant();
if (! $tenant instanceof Tenant) {
abort(404);
}
$mutations->revertToPlatform($record);
$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;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.connection_type_changed',
context: [
'metadata' => [
'provider_connection_id' => (int) $record->getKey(),
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
'from_connection_type' => ProviderConnectionType::Dedicated->value,
'to_connection_type' => ProviderConnectionType::Platform->value,
'source' => 'provider_connection.edit_page',
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
Notification::make()
->title('Connection reverted to platform')
->success()
->send();
})
)
->requireCapability(Capabilities::PROVIDER_MANAGE_DEDICATED)
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('set_default')
->label('Set as default')
->icon('heroicon-o-star')
->color('primary')
->requiresConfirmation()
->visible(fn (ProviderConnection $record): bool => $tenant instanceof Tenant
&& $record->status !== 'disabled'
&& ! $record->is_default
&& ProviderConnection::query()
->where('tenant_id', $tenant->getKey())
->where('provider', $record->provider)
->count() > 1)
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
$tenant = $this->currentTenant();
if (! $tenant instanceof Tenant) {
abort(404);
}
$record->makeDefault();
$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;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.default_set',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
Notification::make()
->title('Default connection updated')
->success()
->send();
})
)
->requireCapability(Capabilities::PROVIDER_MANAGE)
->tooltip('You do not have permission to manage provider connections.')
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('inventory_sync')
->label('Inventory sync')
->icon('heroicon-o-arrow-path')
->color('info')
->visible(function (ProviderConnection $record): bool {
$tenant = $this->currentTenant();
$user = auth()->user();
return $tenant instanceof Tenant
&& $user instanceof User
&& $user->canAccessTenant($tenant)
&& $record->status !== 'disabled';
})
->action(function (ProviderConnection $record, ProviderOperationStartGate $gate): void {
$tenant = $this->currentTenant();
$user = auth()->user();
if (! $tenant instanceof Tenant) {
abort(404);
}
if (! $user instanceof User) {
abort(403);
}
if (! $user->canAccessTenant($tenant)) {
abort(404);
}
$initiator = $user;
$result = $gate->start(
tenant: $tenant,
connection: $record,
operationType: 'inventory_sync',
dispatcher: function (OperationRun $operationRun) use ($tenant, $initiator, $record): void {
ProviderInventorySyncJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) $initiator->getKey(),
providerConnectionId: (int) $record->getKey(),
operationRun: $operationRun,
);
},
initiator: $initiator,
);
if ($result->status === 'scope_busy') {
Notification::make()
->title('Scope is busy')
->body('Another provider operation is already running for this connection.')
->danger()
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
if ($result->status === 'deduped') {
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
if ($result->status === 'blocked') {
$reasonCode = is_string($result->run->context['reason_code'] ?? null)
? (string) $result->run->context['reason_code']
: 'unknown_error';
$reasonEnvelope = app(\App\Support\ReasonTranslation\ReasonPresenter::class)->forOperationRun($result->run, 'notification');
$bodyLines = $reasonEnvelope?->toBodyLines() ?? ['Blocked by provider configuration.'];
Notification::make()
->title('Inventory sync blocked')
->body(implode("\n", $bodyLines))
->warning()
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
})
)
->requireCapability(Capabilities::PROVIDER_RUN)
->tooltip('You do not have permission to run provider operations.')
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('compliance_snapshot')
->label('Compliance snapshot')
->icon('heroicon-o-shield-check')
->color('info')
->visible(function (ProviderConnection $record): bool {
$tenant = $this->currentTenant();
$user = auth()->user();
return $tenant instanceof Tenant
&& $user instanceof User
&& $user->canAccessTenant($tenant)
&& $record->status !== 'disabled';
})
->action(function (ProviderConnection $record, ProviderOperationStartGate $gate): void {
$tenant = $this->currentTenant();
$user = auth()->user();
if (! $tenant instanceof Tenant) {
abort(404);
}
if (! $user instanceof User) {
abort(403);
}
if (! $user->canAccessTenant($tenant)) {
abort(404);
}
$initiator = $user;
$result = $gate->start(
tenant: $tenant,
connection: $record,
operationType: 'compliance.snapshot',
dispatcher: function (OperationRun $operationRun) use ($tenant, $initiator, $record): void {
ProviderComplianceSnapshotJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) $initiator->getKey(),
providerConnectionId: (int) $record->getKey(),
operationRun: $operationRun,
);
},
initiator: $initiator,
);
if ($result->status === 'scope_busy') {
Notification::make()
->title('Scope is busy')
->body('Another provider operation is already running for this connection.')
->danger()
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
if ($result->status === 'deduped') {
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
if ($result->status === 'blocked') {
$reasonCode = is_string($result->run->context['reason_code'] ?? null)
? (string) $result->run->context['reason_code']
: 'unknown_error';
$reasonEnvelope = app(\App\Support\ReasonTranslation\ReasonPresenter::class)->forOperationRun($result->run, 'notification');
$bodyLines = $reasonEnvelope?->toBodyLines() ?? ['Blocked by provider configuration.'];
Notification::make()
->title('Compliance snapshot blocked')
->body(implode("\n", $bodyLines))
->warning()
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
})
)
->requireCapability(Capabilities::PROVIDER_RUN)
->tooltip('You do not have permission to run provider operations.')
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('enable_connection')
->label('Enable connection')
->icon('heroicon-o-play')
->color('success')
->requiresConfirmation()
->visible(fn (ProviderConnection $record): bool => $record->status === 'disabled')
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
$tenant = $this->currentTenant();
if (! $tenant instanceof Tenant) {
return;
}
$hadCredentials = $record->credential()->exists();
$previousStatus = (string) $record->status;
$status = $hadCredentials ? 'connected' : 'needs_consent';
$errorReasonCode = null;
$errorMessage = null;
$record->update([
'status' => $status,
'health_status' => 'unknown',
'last_health_check_at' => null,
'last_error_reason_code' => $errorReasonCode,
'last_error_message' => $errorMessage,
]);
$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;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.enabled',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
'from_status' => $previousStatus,
'to_status' => $status,
'credentials_present' => $hadCredentials,
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
if (! $hadCredentials) {
Notification::make()
->title('Connection enabled (needs consent)')
->body('Grant admin consent before running checks or operations.')
->warning()
->send();
return;
}
Notification::make()
->title('Provider connection enabled')
->success()
->send();
})
)
->requireCapability(Capabilities::PROVIDER_MANAGE)
->tooltip('You do not have permission to manage provider connections.')
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Action::make('disable_connection')
->label('Disable connection')
->icon('heroicon-o-archive-box-x-mark')
->color('danger')
->requiresConfirmation()
->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled')
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
$tenant = $this->currentTenant();
if (! $tenant instanceof Tenant) {
return;
}
$previousStatus = (string) $record->status;
$record->update([
'status' => 'disabled',
]);
$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;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.disabled',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
'from_status' => $previousStatus,
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
Notification::make()
->title('Provider connection disabled')
->warning()
->send();
})
)
->requireCapability(Capabilities::PROVIDER_MANAGE)
->tooltip('You do not have permission to manage provider connections.')
->preserveVisibility()
->apply(),
])
->label('Actions')
->icon('heroicon-o-ellipsis-vertical')
->color('gray'),
];
}
protected function getFormActions(): array
{
$tenant = $this->currentTenant();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return [
$this->getCancelFormAction(),
];
}
$capabilityResolver = app(CapabilityResolver::class);
if ($capabilityResolver->can($user, $tenant, Capabilities::PROVIDER_MANAGE)) {
return parent::getFormActions();
}
return [
$this->getCancelFormAction(),
];
}
protected function handleRecordUpdate(Model $record, array $data): Model
{
$tenant = $record instanceof ProviderConnection
? ($record->tenant ?? $this->currentTenant())
: $this->currentTenant();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
abort(404);
}
$capabilityResolver = app(CapabilityResolver::class);
if (! $capabilityResolver->isMember($user, $tenant)) {
abort(404);
}
if (! $capabilityResolver->can($user, $tenant, Capabilities::PROVIDER_MANAGE)) {
abort(403);
}
return parent::handleRecordUpdate($record, $data);
}
private function currentTenant(): ?Tenant
{
if (isset($this->record) && $this->record instanceof ProviderConnection) {
$recordTenant = ProviderConnectionResource::resolveTenantForRecord($this->record);
if ($recordTenant instanceof Tenant) {
return $recordTenant;
}
}
if (is_string($this->scopedTenantExternalId) && $this->scopedTenantExternalId !== '') {
return Tenant::query()
->where('external_id', $this->scopedTenantExternalId)
->first();
}
$tenant = request()->route('tenant');
if ($tenant instanceof Tenant) {
return $tenant;
}
if (is_string($tenant) && $tenant !== '') {
return Tenant::query()
->where('external_id', $tenant)
->first();
}
$tenantFromCreateResolution = ProviderConnectionResource::resolveTenantForCreate();
if ($tenantFromCreateResolution instanceof Tenant) {
return $tenantFromCreateResolution;
}
return Tenant::current();
}
}