TenantAtlas/apps/platform/app/Filament/System/Pages/Ops/Controls.php
Ahmed Darrazi dcf70b6df8
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m58s
chore: commit workspace changes (session: 242-operational-controls-session-1777207571)
2026-04-26 14:46:12 +02:00

678 lines
25 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\System\Pages\Ops;
use App\Models\AuditLog;
use App\Models\OperationalControlActivation;
use App\Models\PlatformUser;
use App\Models\Tenant;
use App\Models\Workspace;
use App\Services\Audit\AuditRecorder;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Support\Audit\AuditActionId;
use App\Support\Audit\AuditActorSnapshot;
use App\Support\Audit\AuditTargetSnapshot;
use App\Support\Auth\PlatformCapabilities;
use App\Support\OperationalControls\OperationalControlCatalog;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Filament\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException;
class Controls extends Page
{
protected static ?string $navigationLabel = 'Controls';
protected static ?string $title = 'Operational Controls';
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-pause-circle';
protected static string|\UnitEnum|null $navigationGroup = 'Ops';
protected static ?string $slug = 'ops/controls';
protected string $view = 'filament.system.pages.ops.controls';
public static function canAccess(): bool
{
$user = auth('platform')->user();
if (! $user instanceof PlatformUser) {
return false;
}
return $user->hasCapability(PlatformCapabilities::ACCESS_SYSTEM_PANEL)
&& $user->hasCapability(PlatformCapabilities::OPS_CONTROLS_MANAGE);
}
public function mount(): void
{
abort_unless(static::canAccess(), 403);
}
public function getHeader(): ?View
{
return view('filament.system.pages.ops.partials.controls-header', [
'breadcrumbs' => filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : [],
'heading' => $this->getHeading(),
'subheading' => $this->getSubheading(),
]);
}
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
return [
$this->pauseFindingsLifecycleBackfillAction(),
$this->resumeFindingsLifecycleBackfillAction(),
$this->viewHistoryFindingsLifecycleBackfillAction(),
$this->pauseRestoreExecuteAction(),
$this->resumeRestoreExecuteAction(),
$this->viewHistoryRestoreExecuteAction(),
];
}
/**
* @return array<int, array<string, mixed>>
*/
public function controlCards(): array
{
$catalog = app(OperationalControlCatalog::class);
return array_map(
fn (string $controlKey): array => $this->controlSummary($controlKey),
$catalog->keys(),
);
}
/**
* @return array<string, mixed>
*/
public function controlSummary(string $controlKey): array
{
$definition = app(OperationalControlCatalog::class)->definition($controlKey);
$activations = $this->activeActivationsForControl($controlKey);
$effectiveState = $activations->isEmpty() ? 'enabled' : 'paused';
$stateLabel = match (true) {
$activations->contains(fn (OperationalControlActivation $activation): bool => $activation->scope_type === 'global') => 'Paused globally',
$activations->isNotEmpty() => sprintf('Workspace pauses active (%d)', $activations->where('scope_type', 'workspace')->count()),
default => 'Enabled',
};
return [
'control_key' => $controlKey,
'action_slug' => $this->actionSlug($controlKey),
'label' => (string) $definition['label'],
'effective_state' => $effectiveState,
'state_label' => $stateLabel,
'supported_scopes' => $definition['supported_scopes'],
'affected_surfaces' => $definition['affected_surfaces'],
'active_activations' => $activations
->map(fn (OperationalControlActivation $activation): array => $this->activationSummary($activation))
->values()
->all(),
'history_count' => $this->recentAuditEventsForControl($controlKey)->count(),
];
}
/**
* @return array{control_key: string, scope_type: string, workspace_id: ?int, workspace_count: int, tenant_count: int, summary: string}
*/
public function scopeImpactPreview(string $controlKey, string $scopeType, ?int $workspaceId): array
{
$label = app(OperationalControlCatalog::class)->label($controlKey);
if ($scopeType === 'workspace') {
$workspace = is_int($workspaceId)
? Workspace::query()->whereKey($workspaceId)->first()
: null;
if (! $workspace instanceof Workspace) {
return [
'control_key' => $controlKey,
'scope_type' => $scopeType,
'workspace_id' => null,
'workspace_count' => 0,
'tenant_count' => 0,
'summary' => 'Select a workspace to preview the scope impact.',
];
}
$tenantCount = Tenant::query()
->where('workspace_id', (int) $workspace->getKey())
->where('external_id', '!=', 'platform')
->count();
return [
'control_key' => $controlKey,
'scope_type' => $scopeType,
'workspace_id' => (int) $workspace->getKey(),
'workspace_count' => 1,
'tenant_count' => $tenantCount,
'summary' => sprintf('%s will affect workspace %s and %d %s.', $label, $workspace->name, $tenantCount, $tenantCount === 1 ? 'tenant' : 'tenants'),
];
}
$tenantCount = Tenant::query()
->where('external_id', '!=', 'platform')
->count();
$workspaceCount = Tenant::query()
->where('external_id', '!=', 'platform')
->distinct('workspace_id')
->count('workspace_id');
return [
'control_key' => $controlKey,
'scope_type' => 'global',
'workspace_id' => null,
'workspace_count' => $workspaceCount,
'tenant_count' => $tenantCount,
'summary' => sprintf('%s will affect %d %s across %d %s.', $label, $workspaceCount, $workspaceCount === 1 ? 'workspace' : 'workspaces', $tenantCount, $tenantCount === 1 ? 'tenant' : 'tenants'),
];
}
public function pauseFindingsLifecycleBackfillAction(): Action
{
return $this->pauseActionFor('findings.lifecycle.backfill');
}
public function resumeFindingsLifecycleBackfillAction(): Action
{
return $this->resumeActionFor('findings.lifecycle.backfill');
}
public function viewHistoryFindingsLifecycleBackfillAction(): Action
{
return $this->historyActionFor('findings.lifecycle.backfill');
}
public function pauseRestoreExecuteAction(): Action
{
return $this->pauseActionFor('restore.execute');
}
public function resumeRestoreExecuteAction(): Action
{
return $this->resumeActionFor('restore.execute');
}
public function viewHistoryRestoreExecuteAction(): Action
{
return $this->historyActionFor('restore.execute');
}
private function pauseActionFor(string $controlKey): Action
{
$label = app(OperationalControlCatalog::class)->label($controlKey);
return Action::make('pause_'.$this->actionSlug($controlKey))
->label('Pause '.$label)
->icon('heroicon-o-pause')
->color('danger')
->requiresConfirmation()
->modalHeading('Pause '.$label)
->modalDescription('Review the scope impact, reason, and optional expiry before confirming this control change.')
->form($this->pauseFormSchema($controlKey))
->action(function (array $data, AuditRecorder $auditRecorder, WorkspaceAuditLogger $workspaceAuditLogger) use ($controlKey, $label): void {
$actor = $this->controlsActor();
[$scopeType, $workspace, $reasonText, $expiresAt] = $this->normalizePauseInput($data);
$scopeQuery = $this->activationScopeQuery($controlKey, $scopeType, $workspace);
(clone $scopeQuery)
->whereNotNull('expires_at')
->where('expires_at', '<=', now())
->delete();
$activation = (clone $scopeQuery)->notExpired()->first();
$auditAction = $activation instanceof OperationalControlActivation
? AuditActionId::OperationalControlUpdated
: AuditActionId::OperationalControlPaused;
if ($activation instanceof OperationalControlActivation) {
$activation->fill([
'reason_text' => $reasonText,
'expires_at' => $expiresAt,
'updated_by_platform_user_id' => (int) $actor->getKey(),
])->save();
} else {
$activation = OperationalControlActivation::query()->create([
'control_key' => $controlKey,
'scope_type' => $scopeType,
'workspace_id' => $workspace instanceof Workspace ? (int) $workspace->getKey() : null,
'reason_text' => $reasonText,
'expires_at' => $expiresAt,
'created_by_platform_user_id' => (int) $actor->getKey(),
]);
}
$this->recordControlMutation(
auditAction: $auditAction,
activation: $activation,
actor: $actor,
auditRecorder: $auditRecorder,
workspaceAuditLogger: $workspaceAuditLogger,
);
Notification::make()
->title(sprintf('%s %s', $label, $auditAction === AuditActionId::OperationalControlPaused ? 'paused' : 'updated'))
->success()
->send();
});
}
private function resumeActionFor(string $controlKey): Action
{
$label = app(OperationalControlCatalog::class)->label($controlKey);
return Action::make('resume_'.$this->actionSlug($controlKey))
->label('Resume '.$label)
->icon('heroicon-o-play')
->color('gray')
->requiresConfirmation()
->modalHeading('Resume '.$label)
->modalDescription('Remove the selected pause so new starts can proceed again.')
->form($this->resumeFormSchema($controlKey))
->action(function (array $data, AuditRecorder $auditRecorder, WorkspaceAuditLogger $workspaceAuditLogger) use ($controlKey, $label): void {
$actor = $this->controlsActor();
[$scopeType, $workspace] = $this->normalizeResumeInput($data);
$activation = $this->activationScopeQuery($controlKey, $scopeType, $workspace)
->notExpired()
->first();
if (! $activation instanceof OperationalControlActivation) {
Notification::make()
->title(sprintf('%s already enabled', $label))
->warning()
->send();
return;
}
$activationSnapshot = $activation->replicate();
$activationSnapshot->forceFill($activation->getAttributes());
$activation->delete();
$this->recordControlMutation(
auditAction: AuditActionId::OperationalControlResumed,
activation: $activationSnapshot,
actor: $actor,
auditRecorder: $auditRecorder,
workspaceAuditLogger: $workspaceAuditLogger,
);
Notification::make()
->title($label.' resumed')
->success()
->send();
});
}
private function historyActionFor(string $controlKey): Action
{
$label = app(OperationalControlCatalog::class)->label($controlKey);
return Action::make('view_history_'.$this->actionSlug($controlKey))
->label('View '.$label.' history')
->link()
->modalHeading($label.' history')
->modalSubmitAction(false)
->modalCancelActionLabel('Close')
->modalContent(fn () => view('filament.system.pages.ops.partials.operational-control-history', [
'events' => $this->recentAuditEventsForControl($controlKey),
'label' => $label,
]));
}
/**
* @return array<int, \Filament\Schemas\Components\Component>
*/
private function pauseFormSchema(string $controlKey): array
{
return [
Radio::make('scope_type')
->label('Scope')
->options([
'global' => 'Global',
'workspace' => 'One workspace',
])
->default('global')
->live()
->required(),
Select::make('workspace_id')
->label('Workspace')
->searchable()
->visible(fn (callable $get): bool => $get('scope_type') === 'workspace')
->required(fn (callable $get): bool => $get('scope_type') === 'workspace')
->live()
->getSearchResultsUsing(function (string $search): array {
return Workspace::query()
->where('name', 'like', "%{$search}%")
->orderBy('name')
->limit(25)
->pluck('name', 'id')
->all();
})
->getOptionLabelUsing(function ($value): ?string {
if (! is_numeric($value)) {
return null;
}
return Workspace::query()->whereKey((int) $value)->value('name');
}),
Textarea::make('reason_text')
->label('Reason')
->required()
->minLength(5)
->maxLength(500)
->rows(4),
DateTimePicker::make('expires_at')
->label('Expires at')
->seconds(false)
->nullable(),
Placeholder::make('scope_preview')
->label('Scope impact preview')
->content(function (callable $get) use ($controlKey): string {
$preview = $this->scopeImpactPreview(
$controlKey,
(string) ($get('scope_type') ?? 'global'),
is_numeric($get('workspace_id')) ? (int) $get('workspace_id') : null,
);
return (string) $preview['summary'];
}),
];
}
/**
* @return array<int, \Filament\Schemas\Components\Component>
*/
private function resumeFormSchema(string $controlKey): array
{
return [
Radio::make('scope_type')
->label('Scope')
->options([
'global' => 'Global',
'workspace' => 'One workspace',
])
->default('global')
->live()
->required(),
Select::make('workspace_id')
->label('Workspace')
->searchable()
->visible(fn (callable $get): bool => $get('scope_type') === 'workspace')
->required(fn (callable $get): bool => $get('scope_type') === 'workspace')
->getSearchResultsUsing(function (string $search): array {
return Workspace::query()
->where('name', 'like', "%{$search}%")
->orderBy('name')
->limit(25)
->pluck('name', 'id')
->all();
})
->getOptionLabelUsing(function ($value): ?string {
if (! is_numeric($value)) {
return null;
}
return Workspace::query()->whereKey((int) $value)->value('name');
}),
Placeholder::make('scope_preview')
->label('Resume impact preview')
->content(function (callable $get) use ($controlKey): string {
$preview = $this->scopeImpactPreview(
$controlKey,
(string) ($get('scope_type') ?? 'global'),
is_numeric($get('workspace_id')) ? (int) $get('workspace_id') : null,
);
return (string) $preview['summary'];
}),
];
}
private function controlsActor(): PlatformUser
{
$actor = auth('platform')->user();
if (! $actor instanceof PlatformUser) {
abort(403);
}
if (! $actor->hasCapability(PlatformCapabilities::OPS_CONTROLS_MANAGE)) {
abort(403);
}
return $actor;
}
/**
* @return array{0: string, 1: ?Workspace, 2: string, 3: ?CarbonInterface}
*/
private function normalizePauseInput(array $data): array
{
[$scopeType, $workspace] = $this->resolveScopeInput($data);
$reasonText = trim((string) ($data['reason_text'] ?? ''));
if ($reasonText === '') {
throw ValidationException::withMessages([
'reason_text' => 'A reason is required.',
]);
}
$expiresAt = null;
if (filled($data['expires_at'] ?? null)) {
$expiresAt = Carbon::parse((string) $data['expires_at']);
if ($expiresAt->lessThanOrEqualTo(now())) {
throw ValidationException::withMessages([
'expires_at' => 'Expiry must be in the future.',
]);
}
}
return [$scopeType, $workspace, $reasonText, $expiresAt];
}
/**
* @return array{0: string, 1: ?Workspace}
*/
private function normalizeResumeInput(array $data): array
{
return $this->resolveScopeInput($data);
}
/**
* @return array{0: string, 1: ?Workspace}
*/
private function resolveScopeInput(array $data): array
{
$scopeType = (string) ($data['scope_type'] ?? 'global');
if (! in_array($scopeType, ['global', 'workspace'], true)) {
throw ValidationException::withMessages([
'scope_type' => 'Invalid scope selected.',
]);
}
if ($scopeType === 'global') {
return [$scopeType, null];
}
$workspaceId = $data['workspace_id'] ?? null;
if (! is_numeric($workspaceId)) {
throw ValidationException::withMessages([
'workspace_id' => 'A workspace is required for workspace scope.',
]);
}
$workspace = Workspace::query()->whereKey((int) $workspaceId)->first();
if (! $workspace instanceof Workspace) {
throw ValidationException::withMessages([
'workspace_id' => 'The selected workspace could not be found.',
]);
}
return [$scopeType, $workspace];
}
private function activationScopeQuery(string $controlKey, string $scopeType, ?Workspace $workspace): \Illuminate\Database\Eloquent\Builder
{
$query = OperationalControlActivation::query()
->forControl($controlKey)
->where('scope_type', $scopeType);
if ($scopeType === 'workspace') {
$query->where('workspace_id', (int) $workspace?->getKey());
} else {
$query->whereNull('workspace_id');
}
return $query;
}
private function recordControlMutation(
AuditActionId $auditAction,
OperationalControlActivation $activation,
PlatformUser $actor,
AuditRecorder $auditRecorder,
WorkspaceAuditLogger $workspaceAuditLogger,
): void {
$label = app(OperationalControlCatalog::class)->label((string) $activation->control_key);
$summary = sprintf('%s %s', $label, match ($auditAction) {
AuditActionId::OperationalControlPaused => 'paused',
AuditActionId::OperationalControlUpdated => 'updated',
AuditActionId::OperationalControlResumed => 'resumed',
default => 'changed',
});
$metadata = array_filter([
'control_key' => (string) $activation->control_key,
'scope_type' => (string) $activation->scope_type,
'workspace_id' => is_numeric($activation->workspace_id) ? (int) $activation->workspace_id : null,
'reason_text' => $activation->reason_text,
'expires_at' => $activation->expires_at?->toIso8601String(),
'actor_id' => (int) $actor->getKey(),
], static fn (mixed $value): bool => $value !== null && $value !== '');
if ((string) $activation->scope_type === 'global') {
$auditRecorder->record(
action: $auditAction,
context: ['metadata' => $metadata],
actor: AuditActorSnapshot::platform($actor),
target: new AuditTargetSnapshot(
type: 'operational_control',
id: (string) $activation->getKey(),
label: $label,
),
outcome: 'success',
summary: $summary,
);
return;
}
$workspace = Workspace::query()->whereKey((int) $activation->workspace_id)->firstOrFail();
$workspaceAuditLogger->log(
workspace: $workspace,
action: $auditAction,
context: ['metadata' => $metadata],
actor: $actor,
status: 'success',
resourceType: 'operational_control',
resourceId: (string) $activation->getKey(),
targetLabel: $label,
summary: $summary,
);
}
/**
* @return Collection<int, OperationalControlActivation>
*/
private function activeActivationsForControl(string $controlKey): Collection
{
return OperationalControlActivation::query()
->forControl($controlKey)
->notExpired()
->with(['workspace', 'createdBy', 'updatedBy'])
->orderByRaw("CASE WHEN scope_type = 'global' THEN 0 ELSE 1 END")
->orderBy('workspace_id')
->orderBy('id')
->get();
}
/**
* @return array<string, mixed>
*/
private function activationSummary(OperationalControlActivation $activation): array
{
$owner = $activation->updatedBy ?? $activation->createdBy;
$workspaceName = $activation->workspace?->name;
return [
'id' => (int) $activation->getKey(),
'scope_type' => (string) $activation->scope_type,
'scope_label' => (string) $activation->scope_type === 'global'
? 'Global'
: sprintf('Workspace: %s', $workspaceName ?? '#'.(int) $activation->workspace_id),
'workspace_id' => is_numeric($activation->workspace_id) ? (int) $activation->workspace_id : null,
'workspace_name' => $workspaceName,
'reason_text' => (string) $activation->reason_text,
'expires_at' => $activation->expires_at?->toIso8601String(),
'expires_label' => $activation->expires_at?->diffForHumans() ?? 'No expiry',
'owner_name' => $owner?->name ?: $owner?->email ?: 'Unknown operator',
];
}
/**
* @return Collection<int, AuditLog>
*/
private function recentAuditEventsForControl(string $controlKey): Collection
{
return AuditLog::query()
->where('metadata->control_key', $controlKey)
->whereIn('action', [
AuditActionId::OperationalControlPaused->value,
AuditActionId::OperationalControlUpdated->value,
AuditActionId::OperationalControlResumed->value,
AuditActionId::OperationalControlExecutionBlocked->value,
])
->latestFirst()
->limit(10)
->get();
}
private function actionSlug(string $controlKey): string
{
return str_replace('.', '_', $controlKey);
}
}