TenantAtlas/app/Support/Livewire/TrustedState/TrustedStatePolicy.php
ahmido 5ec62cd117 feat: harden livewire trusted state boundaries (#182)
## Summary
- add the shared trusted-state model and resolver helpers for first-slice Livewire and Filament surfaces
- harden managed tenant onboarding, tenant required permissions, and system runbooks against forged or stale public state
- add focused Pest guard and regression coverage plus the complete spec 152 artifact set

## Validation
- `vendor/bin/sail artisan test --compact`
- manual smoke validated on `/admin/onboarding/{onboardingDraft}`
- manual smoke validated on `/admin/tenants/{tenant}/required-permissions`
- manual smoke validated on `/system/ops/runbooks`

## Notes
- Livewire v4.0+ / Filament v5 stack unchanged
- no new panels, routes, assets, or global-search changes
- provider registration remains in `bootstrap/providers.php`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #182
2026-03-18 23:01:14 +00:00

473 lines
20 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Livewire\TrustedState;
use InvalidArgumentException;
final class TrustedStatePolicy
{
public const MANAGED_TENANT_ONBOARDING_WIZARD = 'managed_tenant_onboarding_wizard';
public const TENANT_REQUIRED_PERMISSIONS = 'tenant_required_permissions';
public const SYSTEM_RUNBOOKS = 'system_runbooks';
/**
* @return array{
* name: string,
* state_class: string,
* php_type: string,
* source_of_truth: string,
* used_for_protected_action: bool,
* revalidation_required: bool,
* implementation_markers: list<string>,
* notes: string
* }
*/
private function field(
string $name,
TrustedStateClass $stateClass,
string $phpType,
string $sourceOfTruth,
bool $usedForProtectedAction,
bool $revalidationRequired,
array $implementationMarkers,
string $notes,
): array {
return [
'name' => $name,
'state_class' => $stateClass->value,
'php_type' => $phpType,
'source_of_truth' => $sourceOfTruth,
'used_for_protected_action' => $usedForProtectedAction,
'revalidation_required' => $revalidationRequired,
'implementation_markers' => $implementationMarkers,
'notes' => $notes,
];
}
/**
* @return array<string, array{
* component_name: string,
* plane: string,
* route_anchor: string|null,
* authority_sources: list<string>,
* locked_identities: list<string>,
* locked_identity_fields: list<array{
* name: string,
* state_class: string,
* php_type: string,
* source_of_truth: string,
* used_for_protected_action: bool,
* revalidation_required: bool,
* implementation_markers: list<string>,
* notes: string
* }>,
* mutable_selectors: list<string>,
* mutable_selector_fields: list<array{
* name: string,
* state_class: string,
* php_type: string,
* source_of_truth: string,
* used_for_protected_action: bool,
* revalidation_required: bool,
* implementation_markers: list<string>,
* notes: string
* }>,
* server_derived_authority_fields: list<array{
* name: string,
* state_class: string,
* php_type: string,
* source_of_truth: string,
* used_for_protected_action: bool,
* revalidation_required: bool,
* implementation_markers: list<string>,
* notes: string
* }>,
* forbidden_public_authority_fields: list<string>
* }>
*/
public function firstSlice(): array
{
return [
self::MANAGED_TENANT_ONBOARDING_WIZARD => [
'component_name' => 'Managed tenant onboarding wizard',
'plane' => 'admin_workspace',
'route_anchor' => 'onboarding_draft',
'authority_sources' => [
'route_binding',
'workspace_context',
'persisted_onboarding_draft',
'explicit_scoped_query',
],
'locked_identities' => [
'workspace_id',
'managed_tenant_id',
'onboarding_session_id',
],
'locked_identity_fields' => [
$this->field(
name: 'managedTenantId',
stateClass: TrustedStateClass::LockedIdentity,
phpType: '?int',
sourceOfTruth: 'persisted_onboarding_draft',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
"#[Locked]\n public ?int \$managedTenantId = null;",
'public ?int $managedTenantId = null;',
'currentManagedTenantRecord()',
],
notes: 'Continuity-only tenant identity; protected actions must re-resolve the tenant record before use.',
),
$this->field(
name: 'onboardingSessionId',
stateClass: TrustedStateClass::LockedIdentity,
phpType: '?int',
sourceOfTruth: 'persisted_onboarding_draft',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
"#[Locked]\n public ?int \$onboardingSessionId = null;",
'public ?int $onboardingSessionId = null;',
'currentOnboardingSessionRecord()',
],
notes: 'Continuity-only draft identity; protected actions re-resolve canonical draft truth from the persisted session.',
),
],
'mutable_selectors' => [
'selected_provider_connection_id',
'selected_bootstrap_operation_types',
],
'mutable_selector_fields' => [
$this->field(
name: 'selectedProviderConnectionId',
stateClass: TrustedStateClass::Presentation,
phpType: '?int',
sourceOfTruth: 'explicit_scoped_query',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
'public ?int $selectedProviderConnectionId = null;',
'selectedProviderConnectionId',
'resolveOnboardingDraft(',
],
notes: 'Provider selection is a mutable proposal and must be validated against the canonical draft and workspace before use.',
),
$this->field(
name: 'selectedBootstrapOperationTypes',
stateClass: TrustedStateClass::Presentation,
phpType: 'array<int, string>',
sourceOfTruth: 'presentation_only',
usedForProtectedAction: false,
revalidationRequired: false,
implementationMarkers: [
'public array $selectedBootstrapOperationTypes = [];',
],
notes: 'Wizard UI choice only; it does not define tenant or workspace authority.',
),
],
'server_derived_authority_fields' => [
$this->field(
name: 'workspace',
stateClass: TrustedStateClass::ServerDerivedAuthority,
phpType: 'Workspace',
sourceOfTruth: 'workspace_context',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
'public Workspace $workspace;',
'currentWorkspaceForMember(',
],
notes: 'Workspace membership and current workspace context outrank any client-submitted state.',
),
$this->field(
name: 'onboardingSession',
stateClass: TrustedStateClass::ServerDerivedAuthority,
phpType: '?TenantOnboardingSession',
sourceOfTruth: 'persisted_onboarding_draft',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
'public ?TenantOnboardingSession $onboardingSession = null;',
'resolveOnboardingDraft(',
'currentOnboardingSessionRecord()',
],
notes: 'Draft model instances remain convenience state only and are refreshed from canonical persisted draft truth.',
),
$this->field(
name: 'managedTenant',
stateClass: TrustedStateClass::ServerDerivedAuthority,
phpType: '?Tenant',
sourceOfTruth: 'explicit_scoped_query',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
'public ?Tenant $managedTenant = null;',
'currentManagedTenantRecord()',
],
notes: 'Tenant model instances are display helpers only and must be re-derived from draft or scoped tenant queries.',
),
],
'forbidden_public_authority_fields' => [
'workspace',
'managedTenant',
'onboardingSession',
],
],
self::TENANT_REQUIRED_PERMISSIONS => [
'component_name' => 'Tenant required permissions',
'plane' => 'admin_tenant',
'route_anchor' => 'tenant',
'authority_sources' => [
'route_binding',
'workspace_context',
'explicit_scoped_query',
],
'locked_identities' => [
'scoped_tenant_id',
],
'locked_identity_fields' => [
$this->field(
name: 'scopedTenantId',
stateClass: TrustedStateClass::LockedIdentity,
phpType: '?int',
sourceOfTruth: 'route_binding',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
"#[Locked]\n public ?int \$scopedTenantId = null;",
'public ?int $scopedTenantId = null;',
'trustedScopedTenant()',
],
notes: 'Route-derived tenant identity stays locked for continuity and is re-scoped against workspace context before use.',
),
],
'mutable_selectors' => [
'status',
'type',
'features',
'search',
],
'mutable_selector_fields' => [
$this->field(
name: 'status',
stateClass: TrustedStateClass::Presentation,
phpType: 'string',
sourceOfTruth: 'presentation_only',
usedForProtectedAction: false,
revalidationRequired: false,
implementationMarkers: [
"public string \$status = 'missing';",
],
notes: 'Filter-only state for the permissions view model.',
),
$this->field(
name: 'type',
stateClass: TrustedStateClass::Presentation,
phpType: 'string',
sourceOfTruth: 'presentation_only',
usedForProtectedAction: false,
revalidationRequired: false,
implementationMarkers: [
"public string \$type = 'all';",
],
notes: 'Filter-only state for the permissions view model.',
),
$this->field(
name: 'features',
stateClass: TrustedStateClass::Presentation,
phpType: 'array<int, string>',
sourceOfTruth: 'presentation_only',
usedForProtectedAction: false,
revalidationRequired: false,
implementationMarkers: [
'public array $features = [];',
],
notes: 'Filter-only state for the permissions view model.',
),
$this->field(
name: 'search',
stateClass: TrustedStateClass::Presentation,
phpType: 'string',
sourceOfTruth: 'presentation_only',
usedForProtectedAction: false,
revalidationRequired: false,
implementationMarkers: [
"public string \$search = '';",
],
notes: 'Filter-only state for the permissions view model.',
),
],
'server_derived_authority_fields' => [
$this->field(
name: 'scopedTenant',
stateClass: TrustedStateClass::ServerDerivedAuthority,
phpType: '?Tenant',
sourceOfTruth: 'explicit_scoped_query',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
'resolveScopedTenant()',
'trustedScopedTenant()',
'currentWorkspaceId(request())',
],
notes: 'Tenant scope remains route- and workspace-derived even when mutable filters change.',
),
],
'forbidden_public_authority_fields' => [
'scopedTenant',
],
],
self::SYSTEM_RUNBOOKS => [
'component_name' => 'System runbooks',
'plane' => 'system_platform',
'route_anchor' => null,
'authority_sources' => [
'allowed_tenant_universe',
'explicit_scoped_query',
],
'locked_identities' => [],
'locked_identity_fields' => [],
'mutable_selectors' => [
'findingsTenantId',
'tenantId',
'findingsScopeMode',
'scopeMode',
],
'mutable_selector_fields' => [
$this->field(
name: 'findingsTenantId',
stateClass: TrustedStateClass::Presentation,
phpType: '?int',
sourceOfTruth: 'allowed_tenant_universe',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
'public ?int $findingsTenantId = null;',
'resolveAllowedOrFail($this->findingsTenantId)',
],
notes: 'Single-tenant runbook proposal only; it must be validated against the operator allowed-tenant universe.',
),
$this->field(
name: 'tenantId',
stateClass: TrustedStateClass::Presentation,
phpType: '?int',
sourceOfTruth: 'allowed_tenant_universe',
usedForProtectedAction: false,
revalidationRequired: false,
implementationMarkers: [
'public ?int $tenantId = null;',
],
notes: 'Mirrored display state for the last trusted preflight result.',
),
$this->field(
name: 'findingsScopeMode',
stateClass: TrustedStateClass::Presentation,
phpType: 'string',
sourceOfTruth: 'presentation_only',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
'public string $findingsScopeMode = FindingsLifecycleBackfillScope::MODE_ALL_TENANTS;',
'trustedFindingsScopeFromState(',
],
notes: 'Scope mode remains mutable UI state but protected actions re-normalize it into a trusted scope DTO.',
),
$this->field(
name: 'scopeMode',
stateClass: TrustedStateClass::Presentation,
phpType: 'string',
sourceOfTruth: 'presentation_only',
usedForProtectedAction: false,
revalidationRequired: false,
implementationMarkers: [
'public string $scopeMode = FindingsLifecycleBackfillScope::MODE_ALL_TENANTS;',
],
notes: 'Mirrored display state for the last trusted preflight result.',
),
],
'server_derived_authority_fields' => [
$this->field(
name: 'findingsScope',
stateClass: TrustedStateClass::ServerDerivedAuthority,
phpType: 'FindingsLifecycleBackfillScope',
sourceOfTruth: 'allowed_tenant_universe',
usedForProtectedAction: true,
revalidationRequired: true,
implementationMarkers: [
'trustedFindingsScopeFromFormData(',
'trustedFindingsScopeFromState(',
'resolveAllowedOrFail(',
],
notes: 'Protected actions must convert mutable selector state into a trusted scope DTO via AllowedTenantUniverse.',
),
],
'forbidden_public_authority_fields' => [],
],
];
}
/**
* @return array{
* component_name: string,
* plane: string,
* route_anchor: string|null,
* authority_sources: list<string>,
* locked_identities: list<string>,
* locked_identity_fields: list<array{
* name: string,
* state_class: string,
* php_type: string,
* source_of_truth: string,
* used_for_protected_action: bool,
* revalidation_required: bool,
* implementation_markers: list<string>,
* notes: string
* }>,
* mutable_selectors: list<string>,
* mutable_selector_fields: list<array{
* name: string,
* state_class: string,
* php_type: string,
* source_of_truth: string,
* used_for_protected_action: bool,
* revalidation_required: bool,
* implementation_markers: list<string>,
* notes: string
* }>,
* server_derived_authority_fields: list<array{
* name: string,
* state_class: string,
* php_type: string,
* source_of_truth: string,
* used_for_protected_action: bool,
* revalidation_required: bool,
* implementation_markers: list<string>,
* notes: string
* }>,
* forbidden_public_authority_fields: list<string>
* }
*/
public function forComponent(string $component): array
{
$policy = $this->firstSlice()[$component] ?? null;
if ($policy === null) {
throw new InvalidArgumentException("Unknown trusted-state component [{$component}].");
}
return $policy;
}
/**
* @return list<string>
*/
public function components(): array
{
return array_keys($this->firstSlice());
}
}