PR Body Implements Spec 065 “Tenant RBAC v1” with capabilities-first RBAC, tenant membership scoping (Option 3), and consistent Filament action semantics. Key decisions / rules Tenancy Option 3: tenant switching is tenantless (ChooseTenant), tenant-scoped routes stay scoped, non-members get 404 (not 403). RBAC model: canonical capability registry + role→capability map + Gates for each capability (no role-string checks in UI logic). UX policy: for tenant members lacking permission → actions are visible but disabled + tooltip (avoid click→403). Security still enforced server-side. What’s included Capabilities foundation: Central capability registry (Capabilities::*) Role→capability mapping (RoleCapabilityMap) Gate registration + resolver/manager updates to support tenant-scoped authorization Filament enforcement hardening across the app: Tenant registration & tenant CRUD properly gated Backup/restore/policy flows aligned to “visible-but-disabled” where applicable Provider operations (health check / inventory sync / compliance snapshot) guarded and normalized Directory groups + inventory sync start surfaces normalized Policy version maintenance actions (archive/restore/prune/force delete) gated SpecKit artifacts for 065: spec.md, plan/tasks updates, checklists, enforcement hitlist Security guarantees Non-member → 404 via tenant scoping/membership guards. Member without capability → 403 on execution, even if UI is disabled. No destructive actions execute without proper authorization checks. Tests Adds/updates Pest coverage for: Tenant scoping & membership denial behavior Role matrix expectations (owner/manager/operator/readonly) Filament surface checks (visible/disabled actions, no side effects) Provider/Inventory/Groups run-start authorization Verified locally with targeted vendor/bin/sail artisan test --compact … Deployment / ops notes No new services required. Safe change: behavior is authorization + UI semantics; no breaking route changes intended. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #79
703 lines
34 KiB
PHP
703 lines
34 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources;
|
|
|
|
use App\Filament\Concerns\ScopesGlobalSearchToTenant;
|
|
use App\Filament\Resources\ProviderConnectionResource\Pages;
|
|
use App\Jobs\ProviderComplianceSnapshotJob;
|
|
use App\Jobs\ProviderConnectionHealthCheckJob;
|
|
use App\Jobs\ProviderInventorySyncJob;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Services\Providers\CredentialManager;
|
|
use App\Services\Providers\ProviderOperationStartGate;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Badges\BadgeDomain;
|
|
use App\Support\Badges\BadgeRenderer;
|
|
use App\Support\OperationRunLinks;
|
|
use BackedEnum;
|
|
use Filament\Actions;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use UnitEnum;
|
|
|
|
class ProviderConnectionResource extends Resource
|
|
{
|
|
use ScopesGlobalSearchToTenant;
|
|
|
|
protected static bool $isScopedToTenant = false;
|
|
|
|
protected static ?string $model = ProviderConnection::class;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-link';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Providers';
|
|
|
|
protected static ?string $navigationLabel = 'Connections';
|
|
|
|
protected static ?string $recordTitleAttribute = 'display_name';
|
|
|
|
public static function form(Schema $schema): Schema
|
|
{
|
|
return $schema
|
|
->schema([
|
|
TextInput::make('display_name')
|
|
->label('Display name')
|
|
->required()
|
|
->disabled(fn (): bool => ! Gate::allows(Capabilities::PROVIDER_MANAGE, Tenant::current()))
|
|
->maxLength(255),
|
|
TextInput::make('entra_tenant_id')
|
|
->label('Entra tenant ID')
|
|
->required()
|
|
->maxLength(255)
|
|
->disabled(fn (): bool => ! Gate::allows(Capabilities::PROVIDER_MANAGE, Tenant::current()))
|
|
->rules(['uuid']),
|
|
Toggle::make('is_default')
|
|
->label('Default connection')
|
|
->disabled(fn (): bool => ! Gate::allows(Capabilities::PROVIDER_MANAGE, Tenant::current()))
|
|
->helperText('Exactly one default connection is required per tenant/provider.'),
|
|
TextInput::make('status')
|
|
->label('Status')
|
|
->disabled()
|
|
->dehydrated(false),
|
|
TextInput::make('health_status')
|
|
->label('Health')
|
|
->disabled()
|
|
->dehydrated(false),
|
|
]);
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->modifyQueryUsing(function (Builder $query): Builder {
|
|
$tenantId = Tenant::current()?->getKey();
|
|
|
|
return $query->when($tenantId, fn (Builder $q) => $q->where('tenant_id', $tenantId));
|
|
})
|
|
->defaultSort('display_name')
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('display_name')->label('Name')->searchable(),
|
|
Tables\Columns\TextColumn::make('provider')->label('Provider')->toggleable(),
|
|
Tables\Columns\TextColumn::make('entra_tenant_id')->label('Entra tenant ID')->copyable()->toggleable(),
|
|
Tables\Columns\IconColumn::make('is_default')->label('Default')->boolean(),
|
|
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('last_health_check_at')->label('Last check')->since()->toggleable(),
|
|
])
|
|
->filters([
|
|
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('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('health_status', $value);
|
|
}),
|
|
])
|
|
->actions([
|
|
Actions\ActionGroup::make([
|
|
Actions\EditAction::make(),
|
|
|
|
Actions\Action::make('check_connection')
|
|
->label('Check connection')
|
|
->icon('heroicon-o-check-badge')
|
|
->color('success')
|
|
->visible(function (ProviderConnection $record): bool {
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return false;
|
|
}
|
|
|
|
return $user->canAccessTenant($tenant) && $record->status !== 'disabled';
|
|
})
|
|
->disabled(function (): bool {
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return true;
|
|
}
|
|
|
|
return ! Gate::allows(Capabilities::PROVIDER_RUN, $tenant);
|
|
})
|
|
->tooltip(function (): ?string {
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return null;
|
|
}
|
|
|
|
return Gate::allows(Capabilities::PROVIDER_RUN, $tenant)
|
|
? null
|
|
: 'You do not have permission to run provider operations.';
|
|
})
|
|
->action(function (ProviderConnection $record, ProviderOperationStartGate $gate): void {
|
|
$tenant = Tenant::current();
|
|
$user = auth()->user();
|
|
|
|
abort_unless($tenant instanceof Tenant, 404);
|
|
abort_unless($user instanceof User, 403);
|
|
abort_unless($user->canAccessTenant($tenant), 404);
|
|
abort_unless(Gate::allows(Capabilities::PROVIDER_RUN, $tenant), 403);
|
|
$initiator = $user;
|
|
|
|
$result = $gate->start(
|
|
tenant: $tenant,
|
|
connection: $record,
|
|
operationType: 'provider.connection.check',
|
|
dispatcher: function (OperationRun $operationRun) use ($tenant, $initiator, $record): void {
|
|
ProviderConnectionHealthCheckJob::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 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)),
|
|
])
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if ($result->status === 'deduped') {
|
|
Notification::make()
|
|
->title('Run already queued')
|
|
->body('A connection check is already queued or running.')
|
|
->warning()
|
|
->actions([
|
|
Actions\Action::make('view_run')
|
|
->label('View run')
|
|
->url(OperationRunLinks::view($result->run, $tenant)),
|
|
])
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Connection check queued')
|
|
->body('Health check was queued and will run in the background.')
|
|
->success()
|
|
->actions([
|
|
Actions\Action::make('view_run')
|
|
->label('View run')
|
|
->url(OperationRunLinks::view($result->run, $tenant)),
|
|
])
|
|
->send();
|
|
}),
|
|
|
|
Actions\Action::make('inventory_sync')
|
|
->label('Inventory sync')
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('info')
|
|
->visible(function (ProviderConnection $record): bool {
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return false;
|
|
}
|
|
|
|
return $user->canAccessTenant($tenant) && $record->status !== 'disabled';
|
|
})
|
|
->disabled(function (): bool {
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return true;
|
|
}
|
|
|
|
return ! Gate::allows(Capabilities::PROVIDER_RUN, $tenant);
|
|
})
|
|
->tooltip(function (): ?string {
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return null;
|
|
}
|
|
|
|
return Gate::allows(Capabilities::PROVIDER_RUN, $tenant)
|
|
? null
|
|
: 'You do not have permission to run provider operations.';
|
|
})
|
|
->action(function (ProviderConnection $record, ProviderOperationStartGate $gate): void {
|
|
$tenant = Tenant::current();
|
|
$user = auth()->user();
|
|
|
|
abort_unless($tenant instanceof Tenant, 404);
|
|
abort_unless($user instanceof User, 403);
|
|
abort_unless($user->canAccessTenant($tenant), 404);
|
|
abort_unless(Gate::allows(Capabilities::PROVIDER_RUN, $tenant), 403);
|
|
$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') {
|
|
Notification::make()
|
|
->title('Run already queued')
|
|
->body('An inventory sync is already queued or running.')
|
|
->warning()
|
|
->actions([
|
|
Actions\Action::make('view_run')
|
|
->label('View run')
|
|
->url(OperationRunLinks::view($result->run, $tenant)),
|
|
])
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Inventory sync queued')
|
|
->body('Inventory sync was queued and will run in the background.')
|
|
->success()
|
|
->actions([
|
|
Actions\Action::make('view_run')
|
|
->label('View run')
|
|
->url(OperationRunLinks::view($result->run, $tenant)),
|
|
])
|
|
->send();
|
|
}),
|
|
|
|
Actions\Action::make('compliance_snapshot')
|
|
->label('Compliance snapshot')
|
|
->icon('heroicon-o-shield-check')
|
|
->color('info')
|
|
->visible(function (ProviderConnection $record): bool {
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return false;
|
|
}
|
|
|
|
return $user->canAccessTenant($tenant) && $record->status !== 'disabled';
|
|
})
|
|
->disabled(function (): bool {
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return true;
|
|
}
|
|
|
|
return ! Gate::allows(Capabilities::PROVIDER_RUN, $tenant);
|
|
})
|
|
->tooltip(function (): ?string {
|
|
$tenant = Tenant::current();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return null;
|
|
}
|
|
|
|
return Gate::allows(Capabilities::PROVIDER_RUN, $tenant)
|
|
? null
|
|
: 'You do not have permission to run provider operations.';
|
|
})
|
|
->action(function (ProviderConnection $record, ProviderOperationStartGate $gate): void {
|
|
$tenant = Tenant::current();
|
|
$user = auth()->user();
|
|
|
|
abort_unless($tenant instanceof Tenant, 404);
|
|
abort_unless($user instanceof User, 403);
|
|
abort_unless($user->canAccessTenant($tenant), 404);
|
|
abort_unless(Gate::allows(Capabilities::PROVIDER_RUN, $tenant), 403);
|
|
$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') {
|
|
Notification::make()
|
|
->title('Run already queued')
|
|
->body('A compliance snapshot is already queued or running.')
|
|
->warning()
|
|
->actions([
|
|
Actions\Action::make('view_run')
|
|
->label('View run')
|
|
->url(OperationRunLinks::view($result->run, $tenant)),
|
|
])
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Compliance snapshot queued')
|
|
->body('Compliance snapshot was queued and will run in the background.')
|
|
->success()
|
|
->actions([
|
|
Actions\Action::make('view_run')
|
|
->label('View run')
|
|
->url(OperationRunLinks::view($result->run, $tenant)),
|
|
])
|
|
->send();
|
|
}),
|
|
|
|
Actions\Action::make('set_default')
|
|
->label('Set as default')
|
|
->icon('heroicon-o-star')
|
|
->color('primary')
|
|
->visible(fn (ProviderConnection $record): bool => Gate::allows(Capabilities::PROVIDER_MANAGE, Tenant::current())
|
|
&& $record->status !== 'disabled'
|
|
&& ! $record->is_default)
|
|
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
|
|
$tenant = Tenant::current();
|
|
|
|
abort_unless($tenant instanceof Tenant && Gate::allows(Capabilities::PROVIDER_MANAGE, $tenant), 403);
|
|
|
|
$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();
|
|
}),
|
|
|
|
Actions\Action::make('update_credentials')
|
|
->label('Update credentials')
|
|
->icon('heroicon-o-key')
|
|
->color('primary')
|
|
->modalDescription('Client secret is stored encrypted and will never be shown again.')
|
|
->visible(fn (): bool => Gate::allows(Capabilities::PROVIDER_MANAGE, Tenant::current()))
|
|
->form([
|
|
TextInput::make('client_id')
|
|
->label('Client ID')
|
|
->required()
|
|
->maxLength(255),
|
|
TextInput::make('client_secret')
|
|
->label('Client secret')
|
|
->password()
|
|
->required()
|
|
->maxLength(255),
|
|
])
|
|
->action(function (array $data, ProviderConnection $record, CredentialManager $credentials, AuditLogger $auditLogger): void {
|
|
$tenant = Tenant::current();
|
|
|
|
abort_unless($tenant instanceof Tenant && Gate::allows(Capabilities::PROVIDER_MANAGE, $tenant), 403);
|
|
|
|
$credentials->upsertClientSecretCredential(
|
|
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.credentials_updated',
|
|
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('Credentials updated')
|
|
->success()
|
|
->send();
|
|
}),
|
|
|
|
Actions\Action::make('enable_connection')
|
|
->label('Enable connection')
|
|
->icon('heroicon-o-play')
|
|
->color('success')
|
|
->visible(fn (ProviderConnection $record): bool => Gate::allows(Capabilities::PROVIDER_MANAGE, Tenant::current())
|
|
&& $record->status === 'disabled')
|
|
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
|
|
$tenant = Tenant::current();
|
|
|
|
abort_unless($tenant instanceof Tenant && Gate::allows(Capabilities::PROVIDER_MANAGE, $tenant), 403);
|
|
|
|
$hadCredentials = $record->credential()->exists();
|
|
$status = $hadCredentials ? 'connected' : 'needs_consent';
|
|
$previousStatus = (string) $record->status;
|
|
|
|
$record->update([
|
|
'status' => $status,
|
|
'health_status' => 'unknown',
|
|
'last_health_check_at' => null,
|
|
'last_error_reason_code' => null,
|
|
'last_error_message' => null,
|
|
]);
|
|
|
|
$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();
|
|
}),
|
|
|
|
Actions\Action::make('disable_connection')
|
|
->label('Disable connection')
|
|
->icon('heroicon-o-archive-box-x-mark')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->visible(fn (ProviderConnection $record): bool => Gate::allows(Capabilities::PROVIDER_MANAGE, Tenant::current())
|
|
&& $record->status !== 'disabled')
|
|
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
|
|
$tenant = Tenant::current();
|
|
|
|
abort_unless($tenant instanceof Tenant && Gate::allows(Capabilities::PROVIDER_MANAGE, $tenant), 403);
|
|
|
|
$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();
|
|
}),
|
|
])
|
|
->label('Actions')
|
|
->icon('heroicon-o-ellipsis-vertical')
|
|
->color('gray'),
|
|
])
|
|
->bulkActions([]);
|
|
}
|
|
|
|
public static function getEloquentQuery(): Builder
|
|
{
|
|
$tenantId = Tenant::current()?->getKey();
|
|
|
|
return parent::getEloquentQuery()
|
|
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId))
|
|
->latest('id');
|
|
}
|
|
|
|
public static function getPages(): array
|
|
{
|
|
return [
|
|
'index' => Pages\ListProviderConnections::route('/'),
|
|
'create' => Pages\CreateProviderConnection::route('/create'),
|
|
'edit' => Pages\EditProviderConnection::route('/{record}/edit'),
|
|
];
|
|
}
|
|
}
|