## 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
205 lines
22 KiB
Markdown
205 lines
22 KiB
Markdown
# Feature Specification: Livewire Context Locking and Trusted-State Reduction
|
|
|
|
**Feature Branch**: `152-livewire-context-locking`
|
|
**Created**: 2026-03-18
|
|
**Status**: Draft
|
|
**Input**: User description: "Livewire Context Locking and Trusted-State Reduction"
|
|
|
|
## Spec Scope Fields *(mandatory)*
|
|
|
|
- **Scope**: workspace + tenant + canonical-view
|
|
- **Primary Routes**:
|
|
- `/admin/onboarding`
|
|
- `/admin/onboarding/{onboardingDraft}`
|
|
- `/admin/tenants/{tenant}/required-permissions`
|
|
- System runbook surfaces under `/system/ops/runbooks`
|
|
- Related multi-step or stateful Filament and Livewire surfaces that keep ownership-relevant context in public component state
|
|
- **Data Ownership**:
|
|
- Workspace-scoped onboarding drafts remain workspace-owned workflow records that may reference a tenant and provider connection
|
|
- Tenant-bound pages and widgets remain tenant-scoped views whose entitlement must continue to come from canonical tenant resolution rather than mutable Livewire state
|
|
- System-panel runbook pages remain platform-scoped and may reference a tenant as a scoped target, but the tenant reference remains selector state rather than permission truth
|
|
- This feature introduces no new domain records; it hardens how existing records are exposed to Livewire state and re-resolved during actions
|
|
- **RBAC**:
|
|
- Admin `/admin` workspace and tenant-context surfaces remain governed by workspace membership, tenant entitlement, and canonical capability checks
|
|
- Platform `/system` surfaces remain governed by platform capabilities and permitted tenant-universe rules
|
|
- Non-members, wrong-workspace actors, wrong-tenant actors, and cross-plane access attempts remain deny-as-not-found
|
|
- In-scope actors missing the required capability remain forbidden
|
|
|
|
For canonical-view specs, the spec MUST define:
|
|
|
|
- **Default filter behavior when tenant-context is active**: Canonical viewers may prefill filters from the current tenant context only as a convenience. Filter state must never become the authority for which workspace, tenant, or record the action is allowed to affect.
|
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Every action that uses a tenant, workspace, onboarding draft, provider connection, or selected target from component state must re-resolve that record from canonical server context before reading or mutating anything. Foreign or stale identifiers must fail closed without leaking whether the target exists.
|
|
|
|
## User Scenarios & Testing *(mandatory)*
|
|
|
|
### User Story 1 - Trust ownership-sensitive wizard actions (Priority: P1)
|
|
|
|
As an onboarding operator, I want ownership-sensitive onboarding actions to derive truth from locked or server-resolved context instead of mutable public Livewire state, so that refreshes, forged client payloads, or stale tabs cannot steer the wizard toward the wrong tenant, workspace, or provider record.
|
|
|
|
**Why this priority**: The onboarding wizard is a tier-1 trust surface with the highest concentration of workspace, tenant, draft, and provider context in one Livewire component.
|
|
|
|
**Independent Test**: Can be fully tested by opening the onboarding wizard, mutating ownership-relevant public state in Livewire tests or browser-forged payloads, and confirming that view, verify, bootstrap, cancel, and activation paths still resolve against canonical server truth.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** an operator is on `/admin/onboarding/{onboardingDraft}`, **When** a forged payload changes the draft, tenant, or workspace identity in public component state, **Then** the next action resolves against the canonical route-bound and workspace-bound draft instead of the forged value.
|
|
2. **Given** a mutable selection such as provider connection changes legitimately through the UI, **When** the operator executes verification or bootstrap, **Then** the action re-resolves the selected record within the current draft and current authority boundary before side effects begin.
|
|
3. **Given** a stale or foreign identifier is submitted from the client, **When** the component evaluates the action, **Then** the request fails closed with deny-as-not-found for wrong-scope targets or forbidden for in-scope users missing the required capability.
|
|
|
|
---
|
|
|
|
### User Story 2 - Keep non-wizard stateful pages safe under forged state (Priority: P1)
|
|
|
|
As an operator using tenant-context or system pages with stateful selectors and filters, I want those pages to treat public Livewire state as untrusted input, so that query selectors, filters, and target IDs cannot silently become permission truth.
|
|
|
|
**Why this priority**: The hardening must be repo-wide enough to prevent the wizard from becoming a special case. At least one tenant/admin page and one platform/system page need the same standard.
|
|
|
|
**Independent Test**: Can be fully tested by mutating public selector properties on a tenant-context page and a system runbook page, then verifying that refresh, preflight, and action paths still use canonical route, session, membership, and allowed-universe resolution.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** a tenant-context page has a route-bound tenant and public filter state, **When** the client submits a forged tenant-like value, **Then** the page continues to use the route or canonical resolver as the ownership source of truth.
|
|
2. **Given** a system runbook page exposes a tenant selector as public state, **When** the client submits a tenant outside the allowed platform operator universe, **Then** the request fails closed and no runbook action executes for that tenant.
|
|
3. **Given** a page exposes only search, sort, tab, or filter state, **When** those values change, **Then** the page remains fully usable because non-ownership state may stay mutable.
|
|
|
|
---
|
|
|
|
### User Story 3 - Apply one reusable trusted-state standard to future components (Priority: P2)
|
|
|
|
As a maintainer, I want one explicit decision model for which Livewire state may remain public, which state must be locked, and which state must be derived on the server, so that new components do not reintroduce trust-boundary drift by convention.
|
|
|
|
**Why this priority**: The long-term value of this feature is the reusable pattern and regression matrix, not only the first round of fixes.
|
|
|
|
**Independent Test**: Can be fully tested by applying the standard to multiple representative component families and by guard tests that fail when ownership-relevant model objects or mutable foreign identifiers are introduced without the approved pattern.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** a new Livewire or Filament component introduces ownership-relevant context, **When** it follows the standard, **Then** the component either stores only locked scalar identity or derives the identity server-side on every action.
|
|
2. **Given** a component only needs presentation or filter state, **When** it follows the standard, **Then** that state may remain public and mutable because it does not determine authorization-sensitive truth.
|
|
3. **Given** a future contributor reintroduces public ownership-relevant model objects or mutable foreign IDs on a covered surface, **When** guard tests run, **Then** CI fails with a clear reason.
|
|
|
|
### Edge Cases
|
|
|
|
- A route-bound draft is valid, but public component state carries a different draft or tenant from a stale browser tab.
|
|
- A provider connection is legitimately changed in the UI after the component first mounted; the new value is in-scope and should be allowed only after server re-resolution.
|
|
- A tenant-context page has mutable search and filter state that should stay interactive even though the underlying tenant must remain server-derived.
|
|
- A platform operator submits a tenant ID on a system page that is syntactically valid but outside the operator's allowed tenant universe.
|
|
- A component carries a public Eloquent model for convenience and the serialized model becomes stale or cross-scope after the backing record changes.
|
|
- A forged payload removes or nulls a locked identifier and tries to force the component onto a fallback context.
|
|
- A browser refresh restores a legitimate component with locked identity while leaving non-secret filter and form state functional.
|
|
|
|
## Requirements *(mandatory)*
|
|
|
|
**Constitution alignment (required):** This feature introduces no new Microsoft Graph calls and no new long-running work. It is a trust-boundary hardening feature for stateful Livewire and Filament components. It defines which ownership-relevant values may remain public but locked, which must disappear from public state entirely, and which must be re-derived on the server for every action. Security-relevant failures remain deny-first and auditable through existing action or operation audit coverage where a mutation is attempted.
|
|
|
|
**Constitution alignment (OPS-UX):** This feature does not create a new `OperationRun` type. Existing operation-backed onboarding and system runbook flows continue to use their current Ops-UX contract. The hardening here is that start, refresh, rerun, preflight, and mutation actions must resolve target truth from canonical context before they reuse those existing run surfaces.
|
|
|
|
**Constitution alignment (RBAC-UX):** This feature applies to both tenant/admin `/admin` surfaces and platform `/system` surfaces. Cross-plane access remains deny-as-not-found. Non-members or actors outside workspace or tenant entitlement remain `404`. In-scope actors lacking the required capability remain `403`. Authorization must continue to be enforced server-side through existing policies, capability resolvers, tenant-universe rules, and page-specific guards. Global search is not expanded by this feature. Existing destructive actions such as onboarding draft cancellation remain explicit, confirmation-gated, and capability-checked.
|
|
|
|
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. This feature does not change `/auth/*` behavior.
|
|
|
|
**Constitution alignment (BADGE-001):** Existing status and lifecycle badges remain centralized. This feature must not introduce local badge semantics for trust or lock state.
|
|
|
|
**Constitution alignment (UI-NAMING-001):** Operator-facing wording remains domain-first. Use terms such as `Onboarding draft`, `Verify access`, `Bootstrap`, `Required permissions`, `Preflight`, and `Run`. Implementation-first terms such as `locked property`, `hydration`, or `forged payload` must not become primary operator-facing copy; they belong only in tests and maintainers' guidance.
|
|
|
|
**Constitution alignment (Filament Action Surfaces):** The feature modifies existing Filament and Livewire pages rather than adding new resources. The Action Surface Contract remains satisfied because action labels and availability remain mostly unchanged; the hardening is in how target truth is resolved before those actions execute.
|
|
|
|
**Constitution alignment (UX-001 — Layout & Information Architecture):** No new Filament screen is added. Existing onboarding, tenant page, and system page layouts remain intact. This feature may add or refine hidden trust-handling behavior and validation feedback but must not redesign the information architecture.
|
|
|
|
**Constitution alignment (Filament v5 / Livewire v4):** This feature remains compliant with Filament v5 and Livewire v4.0+. No new panel is introduced, so provider registration remains unchanged in `bootstrap/providers.php`. No globally searchable resource is added or modified. Existing destructive actions in covered surfaces remain `->requiresConfirmation()` and server-authorized.
|
|
|
|
**Constitution alignment (assets and deploy):** No new global or on-demand assets are required. Existing deployment behavior for `php artisan filament:assets` remains unchanged.
|
|
|
|
### Functional Requirements
|
|
|
|
- **FR-152-001**: The system MUST define one repo-wide trusted-state decision model for stateful Livewire and Filament components that distinguishes between non-sensitive presentation state, locked identity state, and server-derived authority state.
|
|
- **FR-152-002**: Ownership-relevant context MUST NOT rely on mutable public Livewire state for authorization-sensitive decisions.
|
|
- **FR-152-003**: Ownership-relevant context includes at least workspace identity, tenant identity, onboarding draft identity, provider connection identity when used to start protected actions, and system-runbook target tenant identity.
|
|
- **FR-152-004**: Tier-1 first-slice component families MUST include the managed tenant onboarding wizard, at least one tenant-context stateful page, and at least one platform/system stateful page.
|
|
- **FR-152-005**: In tier-1 components, route-bound record identity may remain public only when it is locked and the action path still re-resolves the record from canonical server scope before use.
|
|
- **FR-152-006**: In tier-1 components, public Eloquent model objects that represent workspace, tenant, onboarding draft, or provider context MUST NOT be trusted as the final source of truth for protected actions.
|
|
- **FR-152-007**: When a value is needed only for presentation, search, sort, tab selection, or other non-ownership behavior, the value MAY remain public and mutable.
|
|
- **FR-152-008**: When a value determines which tenant, workspace, draft, provider connection, or foreign record is acted upon, the component MUST either store it as locked scalar identity or derive it from route, session, resolver, or persisted draft state on every action.
|
|
- **FR-152-009**: Mutable foreign identifiers submitted from the client MUST be treated as proposals to validate, not as trusted truth.
|
|
- **FR-152-010**: Covered actions MUST re-resolve the proposed target record within the current actor's canonical scope before reading or mutating anything.
|
|
- **FR-152-011**: A wrong-workspace, wrong-tenant, stale, or foreign identifier in a covered action MUST fail closed without leaking whether the foreign target exists.
|
|
- **FR-152-012**: Deny semantics MUST remain consistent: wrong-scope or non-member requests resolve as `404`, while in-scope actors missing the required capability resolve as `403`.
|
|
- **FR-152-013**: The managed tenant onboarding wizard MUST no longer depend on mutable public tenant, workspace, or onboarding-draft state for verification, bootstrap, cancellation, deletion, or activation decisions.
|
|
- **FR-152-014**: Legitimate mutable onboarding selections such as a chosen provider connection or selected bootstrap options MAY remain interactive, but every protected action using those selections MUST re-validate them within the current draft and current authority scope.
|
|
- **FR-152-015**: The tenant required permissions page MUST keep the scoped tenant derived from canonical route or server context rather than allowing filter state or stale public tenant data to redefine the scope.
|
|
- **FR-152-016**: The system runbooks page MUST treat selected tenant IDs as untrusted selector input and MUST validate them against the platform operator's allowed tenant universe before preflight or run execution.
|
|
- **FR-152-017**: The feature MUST define explicit guidance for when `#[Locked]` is the correct pattern, when server derivation is required instead, and when a value should not be public state at all.
|
|
- **FR-152-018**: The feature MUST define that route identity, persisted workflow identity, session-selected workspace, and canonical tenant resolver outputs outrank client-submitted component state.
|
|
- **FR-152-019**: The feature MUST NOT weaken existing resume, refresh, or wizard continuity behavior that depends on legitimate public presentation state.
|
|
- **FR-152-020**: The feature MUST add forged-state and mutated-identifier regression coverage for each tier-1 component family in scope.
|
|
- **FR-152-021**: The feature MUST add at least one positive-path and one negative forged-state authorization test for each first-slice family.
|
|
- **FR-152-022**: The feature MUST add a lightweight guard strategy that fails when covered surfaces introduce ownership-relevant public model objects or mutable foreign identifiers without the approved pattern.
|
|
- **FR-152-023**: The feature MUST document the first-slice exceptions, if any, where a value stays public for usability but is still safe because the server never treats it as final authority.
|
|
- **FR-152-024**: This feature MUST NOT introduce new routes, new capabilities, a new panel, or new global search behavior.
|
|
- **FR-152-025**: Existing audit and operation history for covered actions MUST remain intact; trust-boundary hardening must not bypass or suppress the audit trail already required by those actions.
|
|
|
|
## UI Action Matrix *(mandatory when Filament is changed)*
|
|
|
|
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
|
|---|---|---|---|---|---|---|---|---|---|---|
|
|
| Managed tenant onboarding wizard | `/admin/onboarding`, `/admin/onboarding/{onboardingDraft}` | Existing `Back to workspace`, landing, tenant, and lifecycle actions | Wizard stepper and status callouts | None | None | Existing onboarding start state | Existing lifecycle actions | Existing wizard step navigation and action buttons | Yes | No new operator actions. Hardening applies to route-bound draft, linked tenant, and provider selection handling. Existing destructive actions remain confirmed. |
|
|
| Required permissions page | `/admin/tenants/{tenant}/required-permissions` | Existing tenant and provider-navigation actions only | Page-level filtered view | None | None | Existing empty state or reset-filters path | Not applicable | Filter controls only | No new audit event | Trust hardening is on scoped tenant derivation and filter safety, not on new actions. |
|
|
| System runbooks page | `/system/ops/runbooks` | Existing `Preflight`, `Run…` | Page-level summaries and last-run links | None | None | None | Not applicable | Modal submit / cancel in existing actions | Yes | `Run…` remains destructive-like and confirmed. Tenant selector stays mutable UI state but must never bypass allowed-universe validation. |
|
|
|
|
### Key Entities *(include if feature involves data)*
|
|
|
|
- **Trusted State Class**: The classification of component state as presentation-only, locked identity, or server-derived authority.
|
|
- **Locked Identity**: A public scalar identifier that may remain on the component only to preserve continuity, while remaining immutable from the client and still subject to server re-resolution.
|
|
- **Server-Derived Authority Context**: The authoritative workspace, tenant, draft, or target identity derived from route binding, session, canonical resolvers, or persisted workflow state.
|
|
- **Forged-State Path**: A request path where the client submits altered public component state to try to read or mutate data outside the canonical authority scope.
|
|
- **Tier-1 Stateful Surface**: A Livewire or Filament component family chosen for first-slice hardening because it combines public state with ownership-sensitive actions or selectors.
|
|
|
|
## Success Criteria *(mandatory)*
|
|
|
|
### Measurable Outcomes
|
|
|
|
- **SC-152-001**: In first-slice regression coverage, 100% of covered forged-state attempts on onboarding actions fail closed and do not execute against a foreign or stale target.
|
|
- **SC-152-002**: In first-slice regression coverage, 100% of covered tenant-context and system selector mutations re-resolve against canonical server scope before protected actions execute.
|
|
- **SC-152-003**: Covered tier-1 components retain their existing legitimate refresh and resume behavior while removing mutable public authority state from decision-making.
|
|
- **SC-152-004**: CI contains at least one guard that fails when a covered surface introduces ownership-relevant public model state or mutable foreign identifiers without the approved locking or derivation pattern.
|
|
- **SC-152-005**: The repo gains one documented trusted-state standard that maintainers can apply to future Livewire components without reopening the same trust-boundary design questions.
|
|
|
|
## Non-Goals
|
|
|
|
- Rewriting all Livewire components in one pass
|
|
- Eliminating all public state from all components
|
|
- Replacing existing route models, workspace selectors, or tenant context flows with a new routing model
|
|
- Introducing WebSocket, session-locking, or collaborative editing infrastructure
|
|
- Redesigning onboarding, required permissions, or system runbook UX beyond trust-boundary needs
|
|
- Hardening queued jobs; that remains covered by Spec 149
|
|
- Hardening tenant-owned query canon; that remains covered by Spec 150
|
|
|
|
## Assumptions
|
|
|
|
- Public component state in Livewire is untrusted by default, consistent with the audit constitution.
|
|
- The onboarding wizard remains the highest-risk first-slice surface because it combines route-bound workflow identity, linked tenant state, provider selection, and operation-backed actions.
|
|
- Not every public value is risky; filters, search terms, and other presentation state can remain mutable when they do not determine authority.
|
|
- Existing workspace and tenant resolver seams introduced by Specs 138, 140, 143, 149, and 150 are sufficient foundations for a trusted-state hardening pass.
|
|
- The first implementation slice should prefer bounded high-signal fixes plus regression guards over a sweeping repo-wide refactor.
|
|
|
|
## Dependencies
|
|
|
|
- Spec 138 - Managed Tenant Onboarding Draft Identity & Resume Semantics
|
|
- Spec 140 - Onboarding Lifecycle, Operation Checkpoints & Concurrency MVP
|
|
- Spec 143 - Tenant Lifecycle, Operability, and Context Semantics Foundation
|
|
- Spec 149 - Queued Execution Reauthorization and Scope Continuity
|
|
- Spec 150 - Tenant-Owned Query Canon and Wrong-Tenant Guards
|
|
- Existing Livewire and browser testing infrastructure for onboarding and stateful admin pages
|
|
|
|
## Risks
|
|
|
|
- Overusing locked state where server derivation would be cleaner could preserve too much accidental public authority shape.
|
|
- Overcorrecting by removing all mutable state could degrade legitimate UX for filters, search, and step continuity.
|
|
- Some existing components may rely on public model objects for convenience, making the first pass noisy unless tier-1 scope stays bounded.
|
|
- System-panel selectors may need careful differentiation between allowed UI state and forbidden authority state to avoid unnecessary operator friction.
|
|
|
|
## Final Direction
|
|
|
|
This feature establishes a simple rule: public Livewire state may support continuity and presentation, but it must not become authority. The first slice hardens the onboarding wizard plus representative tenant/admin and system surfaces, defines when to use locked scalar identity versus server derivation, and adds forged-state regression tests so future components cannot drift back toward trusting mutable client-visible context.
|
|
|