satisfy(ActionSurfaceSlot::ListHeader, 'Header actions include capability-gated create.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary row actions are grouped under "More".') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Provider connections intentionally omit bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'List page defines empty-state guidance and CTA.') ->exempt(ActionSurfaceSlot::DetailHeader, 'View page has no additional header mutations in this resource.'); } public static function canCreate(): bool { $tenant = static::resolveTenantForCreate(); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User) { return false; } /** @var CapabilityResolver $resolver */ $resolver = app(CapabilityResolver::class); return $resolver->isMember($user, $tenant) && $resolver->can($user, $tenant, Capabilities::PROVIDER_MANAGE); } protected static function hasTenantCapability(string $capability): bool { $tenant = static::resolveScopedTenant(); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User) { return false; } /** @var CapabilityResolver $resolver */ $resolver = app(CapabilityResolver::class); return $resolver->isMember($user, $tenant) && $resolver->can($user, $tenant, $capability); } protected static function resolveScopedTenant(): ?Tenant { $tenantExternalId = static::resolveRequestedTenantExternalId(); if (is_string($tenantExternalId) && $tenantExternalId !== '') { return static::resolveTenantByExternalId($tenantExternalId); } $routeTenant = request()->route('tenant'); if ($routeTenant instanceof Tenant) { return $routeTenant; } if (is_string($routeTenant) && $routeTenant !== '') { return Tenant::query() ->where('external_id', $routeTenant) ->first(); } $recordTenant = static::resolveTenantFromRouteRecord(); if ($recordTenant instanceof Tenant) { return $recordTenant; } $contextTenantExternalId = static::resolveContextTenantExternalId(); if (is_string($contextTenantExternalId) && $contextTenantExternalId !== '') { return static::resolveTenantByExternalId($contextTenantExternalId); } return static::resolveTenantContextForCurrentPanel(); } public static function resolveTenantForRecord(?ProviderConnection $record = null): ?Tenant { if ($record instanceof ProviderConnection) { $tenant = $record->tenant; if (! $tenant instanceof Tenant && is_numeric($record->tenant_id)) { $tenant = Tenant::query()->whereKey((int) $record->tenant_id)->first(); } if ($tenant instanceof Tenant) { return $tenant; } } return static::resolveScopedTenant(); } public static function resolveRequestedTenantExternalId(): ?string { $queryTenant = request()->query('tenant_id'); if (is_string($queryTenant) && $queryTenant !== '') { return $queryTenant; } return static::resolveTenantExternalIdFromLivewireRequest(); } public static function resolveContextTenantExternalId(): ?string { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); $contextTenantId = app(WorkspaceContext::class)->lastTenantId(request()); if ($workspaceId !== null && $contextTenantId !== null) { $tenant = Tenant::query() ->whereKey($contextTenantId) ->where('workspace_id', (int) $workspaceId) ->first(); if ($tenant instanceof Tenant) { return (string) $tenant->external_id; } } $tenant = static::resolveTenantContextForCurrentPanel(); if ($tenant instanceof Tenant) { return (string) $tenant->external_id; } return null; } public static function resolveTenantForCreate(): ?Tenant { $tenantExternalId = static::resolveRequestedTenantExternalId() ?? static::resolveContextTenantExternalId(); if (! is_string($tenantExternalId) || $tenantExternalId === '') { return null; } $tenant = static::resolveTenantByExternalId($tenantExternalId); $user = auth()->user(); $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if (! $tenant instanceof Tenant || ! $user instanceof User || $workspaceId === null) { return null; } if ((int) $tenant->workspace_id !== (int) $workspaceId) { return null; } if (! $user->canAccessTenant($tenant)) { return null; } return $tenant; } private static function resolveTenantExternalIdFromLivewireRequest(): ?string { if (! request()->headers->has('x-livewire') && ! request()->headers->has('x-livewire-navigate')) { return null; } try { $url = \Livewire\Livewire::originalUrl(); if (is_string($url) && $url !== '') { $externalId = static::extractTenantExternalIdFromUrl($url); if (is_string($externalId) && $externalId !== '') { return $externalId; } } } catch (\Throwable) { // Ignore and fall back to referer. } $referer = request()->headers->get('referer'); if (! is_string($referer) || $referer === '') { return null; } return static::extractTenantExternalIdFromUrl($referer); } private static function extractTenantExternalIdFromUrl(string $url): ?string { $query = parse_url($url, PHP_URL_QUERY); if (is_string($query) && $query !== '') { parse_str($query, $queryParams); $tenantExternalId = $queryParams['tenant_id'] ?? null; if (is_string($tenantExternalId) && $tenantExternalId !== '') { return $tenantExternalId; } } $path = parse_url($url, PHP_URL_PATH); if (! is_string($path) || $path === '') { $path = $url; } if (preg_match('~/(?:admin)/(?:tenants|t)/([0-9a-fA-F-]{36})(?:/|$)~', $path, $matches) !== 1) { return null; } return (string) $matches[1]; } private static function resolveTenantByExternalId(?string $externalId): ?Tenant { if (! is_string($externalId) || $externalId === '') { return null; } return Tenant::query() ->where('external_id', $externalId) ->first(); } private static function resolveTenantFromRouteRecord(): ?Tenant { $record = request()->route('record'); if ($record instanceof ProviderConnection) { return static::resolveTenantForRecord($record); } if (! is_numeric($record)) { return null; } $providerConnection = ProviderConnection::query() ->with('tenant') ->whereKey((int) $record) ->first(); if (! $providerConnection instanceof ProviderConnection) { return null; } return static::resolveTenantForRecord($providerConnection); } private static function applyMembershipScope(Builder $query): Builder { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); $user = auth()->user(); if (! is_int($workspaceId)) { $tenant = static::resolveTenantContextForCurrentPanel(); if ($tenant instanceof Tenant) { $workspaceId = (int) $tenant->workspace_id; } } if (! is_int($workspaceId) || ! $user instanceof User) { return $query->whereRaw('1 = 0'); } return $query ->where('provider_connections.workspace_id', $workspaceId) ->whereExists(function ($membershipScope) use ($user, $workspaceId): void { $membershipScope ->selectRaw('1') ->from('tenants as scoped_tenants') ->join('tenant_memberships as scoped_memberships', function (JoinClause $join) use ($user): void { $join->on('scoped_memberships.tenant_id', '=', 'scoped_tenants.id') ->where('scoped_memberships.user_id', '=', (int) $user->getKey()); }) ->whereColumn('scoped_tenants.id', 'provider_connections.tenant_id') ->where('scoped_tenants.workspace_id', '=', $workspaceId); }); } /** * @return array */ private static function tenantFilterOptions(): array { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); $user = auth()->user(); if (! is_int($workspaceId) || ! $user instanceof User) { return []; } return Tenant::query() ->select(['tenants.external_id', 'tenants.name', 'tenants.environment']) ->join('tenant_memberships as filter_memberships', function (JoinClause $join) use ($user): void { $join->on('filter_memberships.tenant_id', '=', 'tenants.id') ->where('filter_memberships.user_id', '=', (int) $user->getKey()); }) ->where('tenants.workspace_id', $workspaceId) ->orderBy('tenants.name') ->get() ->mapWithKeys(function (Tenant $tenant): array { $environment = strtoupper((string) ($tenant->environment ?? '')); $label = $environment !== '' ? "{$tenant->name} ({$environment})" : (string) $tenant->name; return [(string) $tenant->external_id => $label]; }) ->all(); } private static function sanitizeErrorMessage(?string $value): ?string { if (! is_string($value) || trim($value) === '') { return null; } $normalized = preg_replace('/\s+/', ' ', strip_tags($value)); $normalized = is_string($normalized) ? trim($normalized) : ''; if ($normalized === '') { return null; } return Str::limit($normalized, 120); } private static function providerConnectionTypeLabel(?ProviderConnection $record): string { $connectionType = $record?->connection_type; if ($connectionType instanceof ProviderConnectionType && $connectionType === ProviderConnectionType::Dedicated) { return 'Dedicated connection'; } return 'Platform connection'; } private static function effectiveAppId(?ProviderConnection $record): string { $effectiveAppId = $record?->effectiveAppId(); return filled($effectiveAppId) ? (string) $effectiveAppId : 'Effective app pending review'; } private static function credentialSourceLabel(?ProviderConnection $record): string { if (! $record instanceof ProviderConnection) { return 'Managed centrally by platform'; } $effectiveApp = $record->effectiveAppMetadata(); return match ($effectiveApp['source'] ?? null) { 'dedicated_credential' => 'Dedicated credential', 'review_required' => 'Legacy identity review required', default => 'Managed centrally by platform', }; } private static function migrationReviewLabel(?ProviderConnection $record): string { return $record?->requiresMigrationReview() ? 'Review required' : 'Clear'; } private static function migrationReviewDescription(?ProviderConnection $record): ?string { if (! $record instanceof ProviderConnection) { return null; } $metadata = $record->legacyIdentityMetadata(); $source = $metadata['legacy_identity_classification_source'] ?? null; $result = $metadata['legacy_identity_result'] ?? null; if (! $record->requiresMigrationReview()) { return filled($source) ? sprintf('Classified via %s as %s.', $source, $result ?? 'platform') : null; } $signals = $metadata['legacy_identity_signals'] ?? []; $tenantClientId = $signals['tenant_client_id'] ?? null; $credentialClientId = $signals['credential_client_id'] ?? null; if (filled($tenantClientId) && filled($credentialClientId)) { return sprintf('Legacy tenant app %s conflicts with dedicated app %s.', $tenantClientId, $credentialClientId); } return 'Legacy app evidence conflicts with the current connection and needs explicit review.'; } private static function consentStatusLabelFromState(mixed $state): string { $value = $state instanceof BackedEnum ? $state->value : (is_string($state) ? $state : 'unknown'); return match ($value) { 'required' => 'Required', 'granted' => 'Granted', 'failed' => 'Failed', 'revoked' => 'Revoked', default => 'Unknown', }; } private static function verificationStatusLabelFromState(mixed $state): string { $value = $state instanceof BackedEnum ? $state->value : (is_string($state) ? $state : 'unknown'); return match ($value) { 'pending' => 'Pending', 'healthy' => 'Healthy', 'degraded' => 'Degraded', 'blocked' => 'Blocked', 'error' => 'Error', default => 'Unknown', }; } public static function form(Schema $schema): Schema { return $schema ->schema([ Section::make('Connection') ->schema([ TextInput::make('display_name') ->label('Display name') ->required() ->disabled(fn (): bool => ! static::hasTenantCapability(Capabilities::PROVIDER_MANAGE)) ->maxLength(255), TextInput::make('entra_tenant_id') ->label('Entra tenant ID') ->required() ->maxLength(255) ->disabled(fn (): bool => ! static::hasTenantCapability(Capabilities::PROVIDER_MANAGE)) ->rules(['uuid']), Placeholder::make('connection_type_display') ->label('Connection type') ->content(fn (?ProviderConnection $record): string => static::providerConnectionTypeLabel($record)), Placeholder::make('platform_app_id_display') ->label('Effective app ID') ->content(fn (?ProviderConnection $record): string => static::effectiveAppId($record)), Placeholder::make('effective_app_source_display') ->label('Effective app source') ->content(fn (?ProviderConnection $record): string => static::credentialSourceLabel($record)), Toggle::make('is_default') ->label('Default connection') ->disabled(fn (): bool => ! static::hasTenantCapability(Capabilities::PROVIDER_MANAGE)) ->helperText('Exactly one default connection is required per tenant/provider.'), ]) ->columns(2) ->columnSpanFull(), Section::make('Status') ->schema([ Placeholder::make('consent_status_display') ->label('Consent') ->content(fn (?ProviderConnection $record): string => static::consentStatusLabelFromState($record?->consent_status)), Placeholder::make('verification_status_display') ->label('Verification') ->content(fn (?ProviderConnection $record): string => static::verificationStatusLabelFromState($record?->verification_status)), TextInput::make('status') ->label('Status') ->disabled() ->dehydrated(false), TextInput::make('health_status') ->label('Health') ->disabled() ->dehydrated(false), Placeholder::make('migration_review_status_display') ->label('Migration review') ->content(fn (?ProviderConnection $record): string => static::migrationReviewLabel($record)) ->hint(fn (?ProviderConnection $record): ?string => static::migrationReviewDescription($record)), ]) ->columns(2) ->columnSpanFull(), ]); } public static function infolist(Schema $schema): Schema { return $schema ->schema([ Section::make('Connection') ->schema([ Infolists\Components\TextEntry::make('display_name') ->label('Display name'), Infolists\Components\TextEntry::make('provider') ->label('Provider'), Infolists\Components\TextEntry::make('entra_tenant_id') ->label('Entra tenant ID') ->copyable(), Infolists\Components\TextEntry::make('connection_type') ->label('Connection type') ->formatStateUsing(fn (?ProviderConnectionType $state): string => $state === ProviderConnectionType::Dedicated ? 'Dedicated connection' : 'Platform connection'), Infolists\Components\TextEntry::make('effective_app_id') ->label('Effective app ID') ->state(fn (ProviderConnection $record): string => static::effectiveAppId($record)) ->copyable(), Infolists\Components\TextEntry::make('effective_app_source') ->label('Effective app source') ->state(fn (ProviderConnection $record): string => static::credentialSourceLabel($record)), ]) ->columns(2), Section::make('Status') ->schema([ Infolists\Components\TextEntry::make('consent_status') ->label('Consent') ->formatStateUsing(fn ($state): string => static::consentStatusLabelFromState($state)), Infolists\Components\TextEntry::make('verification_status') ->label('Verification') ->formatStateUsing(fn ($state): string => static::verificationStatusLabelFromState($state)), Infolists\Components\TextEntry::make('status') ->label('Status'), Infolists\Components\TextEntry::make('health_status') ->label('Health'), Infolists\Components\TextEntry::make('migration_review_required') ->label('Migration review') ->formatStateUsing(fn (ProviderConnection $record): string => static::migrationReviewLabel($record)) ->tooltip(fn (ProviderConnection $record): ?string => static::migrationReviewDescription($record)), ]) ->columns(2), ]); } public static function table(Table $table): Table { return $table ->modifyQueryUsing(function (Builder $query): Builder { $query->with(['tenant', 'credential']); $tenantExternalId = static::resolveRequestedTenantExternalId(); if (! is_string($tenantExternalId) || $tenantExternalId === '') { return $query; } return $query->whereHas('tenant', function (Builder $tenantQuery) use ($tenantExternalId): void { $tenantQuery->where('external_id', $tenantExternalId); }); }) ->defaultSort('display_name') ->paginated(TablePaginationProfiles::resource()) ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->recordUrl(fn (ProviderConnection $record): string => static::getUrl('view', ['record' => $record])) ->columns([ Tables\Columns\TextColumn::make('tenant.name') ->label('Tenant') ->description(function (ProviderConnection $record): ?string { $environment = $record->tenant?->environment; if (! is_string($environment) || trim($environment) === '') { return null; } return strtoupper($environment); }) ->url(function (ProviderConnection $record): ?string { $tenant = static::resolveTenantForRecord($record); if (! $tenant instanceof Tenant) { return null; } return TenantResource::getUrl('view', ['record' => $tenant], panel: 'admin'); }), Tables\Columns\TextColumn::make('display_name')->label('Name')->searchable()->sortable(), Tables\Columns\TextColumn::make('provider')->label('Provider')->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('entra_tenant_id')->label('Entra tenant ID')->copyable()->toggleable(isToggledHiddenByDefault: true), Tables\Columns\IconColumn::make('is_default')->label('Default')->boolean(), Tables\Columns\TextColumn::make('connection_type') ->label('Connection type') ->badge() ->formatStateUsing(fn (?ProviderConnectionType $state): string => $state === ProviderConnectionType::Dedicated ? 'Dedicated' : 'Platform') ->color(fn (?ProviderConnectionType $state): string => $state === ProviderConnectionType::Dedicated ? 'info' : 'gray'), Tables\Columns\TextColumn::make('status') ->label('Status') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::ProviderConnectionStatus)) ->color(BadgeRenderer::color(BadgeDomain::ProviderConnectionStatus)) ->icon(BadgeRenderer::icon(BadgeDomain::ProviderConnectionStatus)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::ProviderConnectionStatus)), Tables\Columns\TextColumn::make('health_status') ->label('Health') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::ProviderConnectionHealth)) ->color(BadgeRenderer::color(BadgeDomain::ProviderConnectionHealth)) ->icon(BadgeRenderer::icon(BadgeDomain::ProviderConnectionHealth)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::ProviderConnectionHealth)), Tables\Columns\TextColumn::make('migration_review_required') ->label('Migration review') ->badge() ->formatStateUsing(fn (bool $state): string => $state ? 'Review required' : 'Clear') ->color(fn (bool $state): string => $state ? 'warning' : 'success') ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('last_health_check_at')->label('Last check')->since()->sortable(), Tables\Columns\TextColumn::make('last_error_reason_code') ->label('Last error reason') ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('last_error_message') ->label('Last error message') ->formatStateUsing(fn (?string $state): ?string => static::sanitizeErrorMessage($state)) ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ SelectFilter::make('tenant') ->label('Tenant') ->default(static::resolveScopedTenant()?->external_id) ->options(static::tenantFilterOptions()) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if (! is_string($value) || $value === '') { return $query; } return $query->whereHas('tenant', function (Builder $tenantQuery) use ($value): void { $tenantQuery->where('external_id', $value); }); }), SelectFilter::make('provider') ->label('Provider') ->options([ 'microsoft' => 'Microsoft', ]) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if (! is_string($value) || $value === '') { return $query; } return $query->where('provider_connections.provider', $value); }), SelectFilter::make('status') ->label('Status') ->options([ 'connected' => 'Connected', 'needs_consent' => 'Needs consent', 'error' => 'Error', 'disabled' => 'Disabled', ]) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if (! is_string($value) || $value === '') { return $query; } return $query->where('provider_connections.status', $value); }), SelectFilter::make('health_status') ->label('Health') ->options([ 'ok' => 'OK', 'degraded' => 'Degraded', 'down' => 'Down', 'unknown' => 'Unknown', ]) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if (! is_string($value) || $value === '') { return $query; } return $query->where('provider_connections.health_status', $value); }), Filter::make('default_only') ->label('Default only') ->query(fn (Builder $query): Builder => $query->where('provider_connections.is_default', true)), ]) ->actions([ Actions\ActionGroup::make([ UiEnforcement::forAction( Actions\EditAction::make() ) ->requireCapability(Capabilities::PROVIDER_MANAGE) ->apply(), UiEnforcement::forAction( Actions\Action::make('check_connection') ->label('Check connection') ->icon('heroicon-o-check-badge') ->color('success') ->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled') ->action(function (ProviderConnection $record, StartVerification $verification, \Filament\Tables\Contracts\HasTable $livewire): void { $tenant = static::resolveTenantForRecord($record); $user = auth()->user(); if (! $tenant instanceof Tenant) { abort(404); } if (! $user instanceof User) { abort(403); } $result = $verification->providerConnectionCheck( tenant: $tenant, connection: $record, initiator: $user, ); if ($result->status === 'scope_busy') { Notification::make() ->title('Scope busy') ->body('Another provider operation is already running for this connection.') ->warning() ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), Actions\Action::make('manage_connections') ->label('Manage Provider Connections') ->url(static::getUrl('index', tenant: $tenant)), ]) ->send(); return; } if ($result->status === 'deduped') { OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), Actions\Action::make('manage_connections') ->label('Manage Provider Connections') ->url(static::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([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), Actions\Action::make('manage_connections') ->label('Manage Provider Connections') ->url(static::getUrl('index', tenant: $tenant)), ]) ->send(); return; } OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::queuedToast((string) $result->run->type) ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), ]) ->send(); }) ) ->preserveVisibility() ->requireCapability(Capabilities::PROVIDER_RUN) ->apply(), UiEnforcement::forAction( Actions\Action::make('inventory_sync') ->label('Inventory sync') ->icon('heroicon-o-arrow-path') ->color('info') ->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled') ->action(function (ProviderConnection $record, ProviderOperationStartGate $gate, \Filament\Tables\Contracts\HasTable $livewire): void { $tenant = static::resolveTenantForRecord($record); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User) { return; } $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([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), ]) ->send(); return; } if ($result->status === 'deduped') { OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) ->actions([ 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([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), ]) ->send(); return; } OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::queuedToast((string) $result->run->type) ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), ]) ->send(); }) ) ->preserveVisibility() ->requireCapability(Capabilities::PROVIDER_RUN) ->apply(), UiEnforcement::forAction( Actions\Action::make('compliance_snapshot') ->label('Compliance snapshot') ->icon('heroicon-o-shield-check') ->color('info') ->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled') ->action(function (ProviderConnection $record, ProviderOperationStartGate $gate, \Filament\Tables\Contracts\HasTable $livewire): void { $tenant = static::resolveTenantForRecord($record); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User) { return; } $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([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), ]) ->send(); return; } if ($result->status === 'deduped') { OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) ->actions([ 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([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), ]) ->send(); return; } OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::queuedToast((string) $result->run->type) ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($result->run, $tenant)), ]) ->send(); }) ) ->preserveVisibility() ->requireCapability(Capabilities::PROVIDER_RUN) ->apply(), UiEnforcement::forAction( Actions\Action::make('set_default') ->label('Set as default') ->icon('heroicon-o-star') ->color('primary') ->requiresConfirmation() ->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled' && ! $record->is_default) ->action(function (ProviderConnection $record, AuditLogger $auditLogger): void { $tenant = static::resolveTenantForRecord($record); if (! $tenant instanceof Tenant) { return; } $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(); }) ) ->preserveVisibility() ->requireCapability(Capabilities::PROVIDER_MANAGE) ->apply(), UiEnforcement::forAction( Actions\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 => $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 = static::resolveTenantForRecord($record); if (! $tenant instanceof Tenant) { return; } $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.resource_table', ], ], 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( Actions\Action::make('rotate_dedicated_credential') ->label('Rotate dedicated credential') ->icon('heroicon-o-arrow-path') ->color('primary') ->requiresConfirmation() ->visible(fn (ProviderConnection $record): bool => $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 = static::resolveTenantForRecord($record); if (! $tenant instanceof Tenant) { return; } $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( Actions\Action::make('delete_dedicated_credential') ->label('Delete dedicated credential') ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->visible(fn (ProviderConnection $record): bool => $record->connection_type === ProviderConnectionType::Dedicated && $record->credential()->exists()) ->action(function (ProviderConnection $record, ProviderConnectionMutationService $mutations): void { $tenant = static::resolveTenantForRecord($record); if (! $tenant instanceof Tenant) { return; } $mutations->deleteDedicatedCredential($record); Notification::make() ->title('Dedicated credential deleted') ->warning() ->send(); }) ) ->requireCapability(Capabilities::PROVIDER_MANAGE_DEDICATED) ->preserveVisibility() ->apply(), UiEnforcement::forAction( Actions\Action::make('revert_to_platform') ->label('Revert to platform') ->icon('heroicon-o-arrow-uturn-left') ->color('gray') ->requiresConfirmation() ->visible(fn (ProviderConnection $record): bool => $record->connection_type === ProviderConnectionType::Dedicated) ->action(function (ProviderConnection $record, ProviderConnectionMutationService $mutations, AuditLogger $auditLogger): void { $tenant = static::resolveTenantForRecord($record); if (! $tenant instanceof Tenant) { return; } $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.resource_table', ], ], 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( Actions\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 = static::resolveTenantForRecord($record); if (! $tenant instanceof Tenant) { return; } $hadCredentials = $record->credential()->exists(); $previousStatus = (string) $record->status; $status = $hadCredentials ? 'connected' : 'error'; $errorReasonCode = $hadCredentials ? null : ProviderReasonCodes::ProviderCredentialMissing; $errorMessage = $hadCredentials ? null : 'Provider connection credentials are missing.'; $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 (credentials missing)') ->body('Add credentials before running checks or operations.') ->warning() ->send(); return; } Notification::make() ->title('Provider connection enabled') ->success() ->send(); }) ) ->preserveVisibility() ->requireCapability(Capabilities::PROVIDER_MANAGE) ->apply(), UiEnforcement::forAction( Actions\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 = static::resolveTenantForRecord($record); 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(); }) ) ->preserveVisibility() ->requireCapability(Capabilities::PROVIDER_MANAGE) ->apply(), ]) ->label('More') ->icon('heroicon-o-ellipsis-vertical') ->color('gray'), ]) ->bulkActions([]); } public static function getEloquentQuery(): Builder { $query = parent::getEloquentQuery() ->with('tenant'); return static::applyMembershipScope($query) ->latest('provider_connections.id'); } public static function getPages(): array { return [ 'index' => Pages\ListProviderConnections::route('/'), 'create' => Pages\CreateProviderConnection::route('/create'), 'view' => Pages\ViewProviderConnection::route('/{record}'), 'edit' => Pages\EditProviderConnection::route('/{record}/edit'), ]; } private static function normalizeTenantExternalId(mixed $tenant): ?string { if ($tenant instanceof Tenant) { return (string) $tenant->external_id; } if (is_string($tenant) && $tenant !== '') { return $tenant; } if (is_numeric($tenant)) { $tenantModel = Tenant::query()->whereKey((int) $tenant)->first(); if ($tenantModel instanceof Tenant) { return (string) $tenantModel->external_id; } } return null; } /** * @param array $parameters */ public static function getUrl(?string $name = null, array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false): string { $panel ??= 'admin'; $tenantExternalId = null; if (array_key_exists('tenant', $parameters)) { $tenantExternalId = static::normalizeTenantExternalId($parameters['tenant']); unset($parameters['tenant']); } if ($tenantExternalId === null && $tenant instanceof Tenant) { $tenantExternalId = (string) $tenant->external_id; } if ($tenantExternalId === null) { $record = $parameters['record'] ?? null; if ($record instanceof ProviderConnection) { $tenantExternalId = static::resolveTenantForRecord($record)?->external_id; } } if ($tenantExternalId === null) { $tenantExternalId = static::resolveScopedTenant()?->external_id; } if (! array_key_exists('tenant_id', $parameters) && is_string($tenantExternalId) && $tenantExternalId !== '') { $parameters['tenant_id'] = $tenantExternalId; } return parent::getUrl($name, $parameters, $isAbsolute, $panel, null, $shouldGuessMissingParameters); } }