## Summary - introduce a shared tenant-owned query and record-resolution canon for first-slice Filament resources - harden direct views, row actions, bulk actions, relation managers, and workspace-admin canonical viewers against wrong-tenant access - add registry-backed rollout metadata, search posture handling, architectural guards, and focused Pest coverage for scope parity and 404/403 semantics ## Included - Spec 150 package under `specs/150-tenant-owned-query-canon-and-wrong-tenant-guards/` - shared support classes: `TenantOwnedModelFamilies`, `TenantOwnedQueryScope`, `TenantOwnedRecordResolver` - shared Filament concern: `InteractsWithTenantOwnedRecords` - resource/page/policy hardening across findings, policies, policy versions, backup schedules, backup sets, restore runs, inventory items, and Entra groups - additional regression coverage for canonical tenant state, wrong-tenant record resolution, relation-manager congruence, and action-surface guardrails ## Validation - `vendor/bin/sail artisan test --compact` passed - full suite result: `2733 passed, 8 skipped` - formatting applied with `vendor/bin/sail bin pint --dirty --format agent` ## Notes - Livewire v4.0+ compliant via existing Filament v5 stack - provider registration remains in `bootstrap/providers.php` - globally searchable first-slice posture: Entra groups scoped; policies and policy versions explicitly disabled - destructive actions continue to use confirmation and policy authorization - no new Filament assets added; existing deployment flow remains unchanged, including `php artisan filament:assets` when registered assets are used Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #180
6.8 KiB
6.8 KiB
Research: Tenant-Owned Query Canon and Wrong-Tenant Guards
Decision 1: Build the canon around a shared tenant-owned query helper, not page-local where('tenant_id', ...)
- Decision: The first implementation slice should introduce a shared tenant-owned query and record-resolution contract for representative model families, layered on top of the existing panel tenant resolution rules from
ResolvesPanelTenantContextandOperateHubShell. - Rationale: The repo already has stable context resolution semantics, but many resources still enforce tenant scope by repeating
where('tenant_id', ...)insidegetEloquentQuery(), list pages, relation managers, and action code paths. A shared contract lets list, detail, row action, bulk action, relation manager, and search paths use one source of truth instead of drifting independently. - Alternatives considered:
- Policy-only enforcement: rejected because policies do not guarantee list/query parity, relation-manager parity, or forged identifier protection on their own.
- Route middleware only: rejected because many wrong-tenant paths happen after the page is already loaded, especially in table actions and relation managers.
- Global Eloquent scope on every tenant-owned model: rejected for the first slice because admin canonical viewers and explicit exceptions need more precise, surface-aware control.
Decision 2: Use TenantOwnedTables as the authoritative inventory for mandatory tenant-owned families, then map that inventory to representative Filament surfaces
- Decision: The rollout inventory should start from
App\Support\WorkspaceIsolation\TenantOwnedTablesand then map each table to the corresponding model/resource/relation-manager surfaces. The first implementation slice should cover representative tier-1 families: policies, policy versions, backup schedules, backup sets plus backup items relation manager, restore runs, findings, inventory items, and Entra groups. - Rationale:
TenantOwnedTablesalready encodes the repo's explicit tenant-owned data boundary. Planning from that list avoids guessing which families are in scope and creates a stable bridge between database ownership rules and UI/query enforcement. The listed resources also already have regression coverage and existing tenant-resolution logic that can be unified. - Alternatives considered:
- Start from all Filament resources under
app/Filament/Resources: rejected because not every resource is tenant-owned and that would blur workspace-owned exceptions. - Start from one resource family only: rejected because the spec is intended to prove a reusable architectural pattern, not a local fix.
- Start from all Filament resources under
Decision 3: Treat workspace-admin tenant-owned viewers as explicit exceptions with strict record-level entitlement checks
- Decision: Workspace-admin canonical viewers may continue to open tenant-owned records outside
/admin/t/{tenant}/..., but only through explicit record-level lookup that verifies workspace membership, tenant entitlement, and owning-tenant congruence before rendering the record. - Rationale: The constitution explicitly allows tenantless canonical views when entitlement checks remain tenant-safe. The repo already does this for
OperationRunPolicy,EntraGroupResourceadmin behavior, and several managed-tenant routes. The plan should preserve those legitimate deep-link cases while banning broader list-style fallback queries. - Alternatives considered:
- Force all tenant-owned views under
/admin/t/{tenant}/...: rejected because the current product has legitimate workspace-admin inspection paths and forcing a route migration is outside this spec. - Allow broad admin viewers with policy-only checks: rejected because it recreates the original wrong-tenant drift problem.
- Force all tenant-owned views under
Decision 4: Keep global search enabled only where list/detail parity can be guaranteed; otherwise disable it deliberately
- Decision: The rollout should adopt
ScopesGlobalSearchToTenantwhere list/detail parity is already achievable and leave search disabled for resources that still lack safe parity. - Rationale: The trait already resolves tenant context through admin remembered-tenant semantics and tenant-panel context, and it fails closed with
whereRaw('1 = 0')when scope is unavailable. Existing tests already document deliberate search disablement for resources such as policies and policy versions when parity is not guaranteed. The plan should formalize that posture rather than assume all tenant-owned resources must be searchable. - Alternatives considered:
- Enable global search for every tenant-owned resource: rejected because some resources still use broader or inconsistent resolution paths.
- Disable global search repo-wide for tenant-owned resources: rejected because safe search already exists for some families and is useful when parity is enforced.
Decision 5: Extend existing guardrails with a tenant-owned query guard and a reusable wrong-tenant regression matrix
- Decision: The implementation should extend the existing guard strategy, not invent a parallel system. Concretely, add a lightweight tenant-owned query guard for representative surfaces and pair it with a reusable wrong-tenant regression matrix covering index, detail, row action, bulk action, relation manager, and safe-search scenarios.
- Rationale: The repo already has architectural guards such as
AdminTenantResolverGuardTest,NoAdHocFilamentAuthPatternsTest, and multiple RBAC parity tests. Reusing that style keeps the hardening enforceable in CI without demanding a one-off audit every time a resource changes. - Alternatives considered:
- Exhaustive grep guard against every
where('tenant_id'...)in the repo: rejected because legitimate service-layer and job-level tenant filters would create too much noise. - Regression tests without a guard: rejected because new files could reintroduce forbidden patterns silently.
- Exhaustive grep guard against every
Decision 6: Relation managers must anchor scope to the owner record, then prove tenant congruence on action targets
- Decision: Relation managers and embedded tables should derive their allowed dataset from the owner record relationship and add explicit tenant congruence checks on action targets instead of depending only on ambient
Tenant::current()state. - Rationale: The current codebase shows both patterns. Some relation managers scope only through the page tenant, while others rely on owner-record relationships plus manual congruence checks. The owner-record anchor is more robust because it prevents drift when a forged record identifier is submitted or when page context and related-record lookup diverge.
- Alternatives considered:
- Ambient tenant only via
Tenant::current(): rejected because it is vulnerable to broader related-record paths when owner-record and action-target parity is not enforced. - No relation-manager-specific rule: rejected because relation managers are one of the stated wrong-tenant risk surfaces in the spec.
- Ambient tenant only via