TenantAtlas/apps/platform/app/Filament/Resources/ProviderConnectionResource/Pages/EditProviderConnection.php
ahmido acc8947384 feat: harden governance action semantics (#229)
## Summary
- add the Spec 194 governance action catalog, friction classes, reason policies, and regression guards
- align exception, review, evidence, finding, tenant, provider connection, and system run actions to the shared semantics model
- add focused feature, RBAC, audit, unit, and browser coverage, including the tenant detail triage header consistency update

## Verification
- ran the focused Spec 194 verification pack from the quickstart and task plan
- ran targeted tenant triage coverage after the detail-header update
- ran `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`

## Filament Notes
- Filament v5 / Livewire v4 compliance preserved
- provider registration remains in `apps/platform/bootstrap/providers.php`
- globally searchable resources were not changed
- destructive actions remain confirmation-gated and server-authorized
- no new Filament assets were introduced; the existing `cd apps/platform && php artisan filament:assets` deploy step stays unchanged

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #229
2026-04-12 21:21:44 +00:00

314 lines
11 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(),
ProviderConnectionResource::makeCheckConnectionAction(),
ProviderConnectionResource::makeInventorySyncAction(),
ProviderConnectionResource::makeComplianceSnapshotAction(),
ProviderConnectionResource::makeSetDefaultAction(),
ProviderConnectionResource::makeEnableDedicatedOverrideAction(
source: 'provider_connection.edit_page',
modalDescription: 'Dedicated credentials are stored encrypted and reset consent to the dedicated app registration.',
),
ProviderConnectionResource::makeRotateDedicatedCredentialAction(
modalDescription: 'Stores a replacement dedicated client secret and refreshes dedicated identity state.',
),
ProviderConnectionResource::makeDeleteDedicatedCredentialAction(
modalDescription: 'Deletes the dedicated credential and leaves the connection blocked until a replacement is added or the type is reverted.',
),
ProviderConnectionResource::makeRevertToPlatformAction(
source: 'provider_connection.edit_page',
modalDescription: 'Reverts the connection to the platform-managed identity and removes any dedicated credential.',
),
ProviderConnectionResource::makeEnableConnectionAction(),
ProviderConnectionResource::makeDisableConnectionAction(),
])
->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();
}
}