## 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
82 lines
9.0 KiB
Markdown
82 lines
9.0 KiB
Markdown
# Research: Livewire Context Locking and Trusted-State Reduction
|
|
|
|
## Decision 1: Use a three-lane trusted-state model for stateful Livewire surfaces
|
|
|
|
- Decision: Classify component state into three explicit lanes: presentation state, locked identity state, and server-derived authority state.
|
|
- Rationale: The current repo already mixes harmless UI state with ownership-sensitive context. The problem is not that all public state is wrong; it is that authority-sensitive state is not separated from convenience state. A three-lane model keeps UX flexible while making trust boundaries explicit.
|
|
- Alternatives considered:
|
|
- Lock every public property: rejected because filters, search terms, and step-local form state must stay mutable for normal UX.
|
|
- Derive everything on every render with no persisted public identity: rejected because multi-step flows such as onboarding still need continuity across requests.
|
|
- Keep the current model-object-heavy pattern and rely on authorization only: rejected because Livewire public properties are still untrusted input and expose model metadata to the browser.
|
|
|
|
## Decision 2: Prefer locked scalar IDs over public Eloquent models for ownership-relevant continuity
|
|
|
|
- Decision: Route-bound or continuity-relevant identities may stay public only as locked scalar IDs. Public Eloquent models should be replaced or treated as temporary compatibility exceptions in covered surfaces.
|
|
- Rationale: Livewire v4 supports `#[Locked]` for model IDs and documents that public properties are client-mutable by default. Public Eloquent models keep IDs safer than raw mutable integers, but they still expose class and relationship metadata to the browser and can preserve stale assumptions. Locked scalar IDs plus server-side re-resolution are the clearer long-term pattern for trust-sensitive flows.
|
|
- Alternatives considered:
|
|
- Rely on public Eloquent model auto-locking everywhere: rejected because it still exposes model class names and can encourage stale-query trust.
|
|
- Use protected properties for sensitive state: rejected because Livewire does not persist protected properties across requests.
|
|
- Store foreign IDs as normal public integers and only authorize later: rejected because this recreates the exact forged-state class of bug the spec is trying to prevent.
|
|
|
|
## Decision 3: Server-derived authority must outrank component state on every protected action
|
|
|
|
- Decision: Protected actions must re-resolve tenant, workspace, onboarding draft, and selected target records from canonical server seams before reading or mutating data.
|
|
- Rationale: Livewire documents that public properties should be treated like request input. The repo already has good canonical seams: `WorkspaceContext`, `OperateHubShell`, `ResolvesPanelTenantContext`, `TenantRequiredPermissions::resolveScopedTenant()`, onboarding draft resolvers, and `AllowedTenantUniverse` for platform selectors. Reusing these seams is safer than inventing a parallel trust layer.
|
|
- Alternatives considered:
|
|
- Validate only in `mount()`: rejected because stale tabs and forged payloads happen after mount.
|
|
- Add a broad Livewire middleware abstraction first: rejected for the first slice because the repo already has concrete per-surface resolvers to reuse.
|
|
- Route all state through hidden inputs or Alpine-only state: rejected because it hides the problem rather than solving the authority boundary.
|
|
|
|
## Decision 4: First slice covers onboarding wizard, one tenant-context page, and one system page
|
|
|
|
- Decision: The first slice should harden three representative families: `ManagedTenantOnboardingWizard`, `TenantRequiredPermissions`, and `System\Pages\Ops\Runbooks`.
|
|
- Rationale: This gives one high-risk multi-step workflow, one tenant-context admin page, and one platform/system selector page. That is enough surface diversity to establish a reusable standard without turning the first implementation into an unbounded repo-wide cleanup.
|
|
- Alternatives considered:
|
|
- Onboarding wizard only: rejected because that would not establish the cross-plane pattern the spec promises.
|
|
- All Livewire and Filament pages at once: rejected because it would create high churn and low signal in the first slice.
|
|
- Only system pages: rejected because the trust boundary was first identified in admin onboarding and tenant-context flows.
|
|
|
|
## Decision 5: Legitimate selector state may remain mutable if the server treats it as a proposal
|
|
|
|
- Decision: Values such as selected provider connection IDs, selected tenant IDs in system runbooks, and filter/search state may remain public and mutable if every protected action re-validates them inside the current scope before use.
|
|
- Rationale: Not every mutable value is authority. Some values are legitimate user choices. The correct boundary is whether the server treats the value as final truth or as input to validate.
|
|
- Alternatives considered:
|
|
- Lock selected provider connection IDs after mount: rejected because onboarding legitimately changes provider selection during the flow.
|
|
- Make all system-page selectors read-only after first load: rejected because that would break the intended operator workflow.
|
|
- Store selectors only in the query string: rejected because query strings are just as untrusted unless revalidated and would not simplify the trust problem.
|
|
|
|
## Decision 6: Extend existing forged-state and guard patterns instead of creating a parallel guard system
|
|
|
|
- Decision: Reuse and extend the current guard test family for admin tenant resolver safety and the existing forged foreign-tenant action tests already used in backup schedules, findings, and other RBAC slices.
|
|
- Rationale: The repo already enforces resolver patterns and deny semantics through targeted Pest guards. Extending those tests keeps the hardening style consistent and avoids a second, overlapping architectural-guard system.
|
|
- Alternatives considered:
|
|
- Introduce a new generic Livewire state linter: rejected for the first slice because it would likely be noisy and less grounded in real trust failures.
|
|
- Rely only on feature tests with no guards: rejected because guard tests are the cheapest way to stop recurrence.
|
|
- Add browser-only forged-state coverage: rejected because Livewire component tests and guard tests are a faster and more deterministic first layer.
|
|
|
|
## Decision 7: Prefer computed or resolver-backed model access over persisted public model objects
|
|
|
|
- Decision: Covered components should expose model-backed data to views through computed accessors, local method resolution, or server-backed helper methods instead of storing those models as authoritative public state.
|
|
- Rationale: Livewire warns that public models are dehydrated to JSON, expose system information, and lose query constraints. Computed accessors or resolver-backed methods reduce stale-model assumptions while keeping the UI expressive.
|
|
- Alternatives considered:
|
|
- Keep model objects public and rely on Livewire auto-locking only: rejected because auto-locking protects ID tampering but not stale assumptions or metadata exposure.
|
|
- Re-query models ad hoc in Blade views: rejected because it hides data access in templates and creates consistency problems.
|
|
- Serialize custom DTOs for every surface immediately: rejected as overdesign for the first slice.
|
|
|
|
## Decision 8: Fail-closed semantics remain route- and plane-aware
|
|
|
|
- Decision: Wrong-scope or non-member forged-state paths resolve as `404`, while in-scope actors missing capability resolve as `403`. Platform-plane selectors must apply the same principle through allowed-universe validation.
|
|
- Rationale: This matches the constitution and existing RBAC-UX behavior. Trusted-state hardening should reinforce, not reinterpret, deny semantics.
|
|
- Alternatives considered:
|
|
- Return validation errors for forged target IDs: rejected because that leaks too much about scope boundaries.
|
|
- Always throw a generic locked-property exception to the user: rejected because not all state should be locked, and server-derived failures still need policy-consistent deny semantics.
|
|
- Normalize all failures to `403`: rejected because it breaks deny-as-not-found isolation semantics.
|
|
|
|
## Decision 9: Reusable helper semantics should stay executable, not prose-only
|
|
|
|
- Decision: The first-slice contract should expose reusable helper semantics through executable APIs on `TrustedStateClass` and `TrustedStateResolver`, with guard tests asserting those semantics.
|
|
- Rationale: The pattern needs to be teachable to future component work without relying on a human re-reading this research note. Enum helpers such as `allowsClientMutation()` / `requiresServerRevalidation()` and resolver accessors for required authority sources make the trust model mechanically checkable in CI.
|
|
- Alternatives considered:
|
|
- Keep helper guidance only in spec prose: rejected because architectural drift would be detected too late.
|
|
- Add a large generic Livewire analyzer: rejected because the first slice only needs explicit semantics for the approved pattern.
|
|
- Encode helper semantics solely in component-local methods: rejected because that does not create a reusable repository standard. |