TenantAtlas/specs/135-canonical-tenant-context-resolution/research.md
ahmido cc93329672 feat: canonical tenant context resolution (#164)
## Summary
- introduce a canonical admin tenant filter-state helper and route all in-scope workspace-admin tenant resolution through `OperateHubShell::activeEntitledTenant()`
- align operations monitoring, operation-run deep links, Entra group admin list/view/search behavior, and shared context-bar rendering with the documented scope contract
- add the Spec 135 design artifacts, architecture note, focused guardrail coverage, and non-regression tests for filter persistence, direct-record access, and global search safety

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Feature/Monitoring/OperationsKpiHeaderTenantContextTest.php tests/Feature/Monitoring/OperationsTenantScopeTest.php tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php tests/Feature/Spec085/OperationsIndexHeaderTest.php tests/Feature/Spec085/RunDetailBackAffordanceTest.php tests/Feature/Filament/OperationRunListFiltersTest.php tests/Feature/Filament/EntraGroupAdminScopeTest.php tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php tests/Feature/DirectoryGroups/BrowseGroupsTest.php tests/Feature/Filament/EntraGroupEnterpriseDetailPageTest.php tests/Feature/Filament/PolicyVersionResolvedReferenceLinksTest.php tests/Feature/Filament/EntraGroupResolvedReferencePresentationTest.php tests/Feature/Guards/AdminTenantResolverGuardTest.php tests/Feature/OpsUx/OperateHubShellTest.php tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/TableStatePersistenceTest.php tests/Feature/Filament/TenantScopingTest.php tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php tests/Unit/Support/References/CapabilityAwareReferenceResolverTest.php`

## Notes
- Filament v5 remains on Livewire v4.0+ compliant surfaces only.
- No provider registration changes were needed; Laravel 12 provider registration remains in `bootstrap/providers.php`.
- Entra group global search remains enabled and is now scoped to the canonical admin tenant contract.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #164
2026-03-11 21:24:28 +00:00

6.8 KiB
Raw Blame History

Research: Spec 135 Canonical Tenant Context Resolution

Decision 1: Preserve explicit split semantics between panels

Decision: Keep tenant-panel and workspace-admin tenant resolution as two explicit concepts. Tenant-panel flows continue to use panel-native tenant context. Workspace-admin flows continue to use OperateHubShell::activeEntitledTenant(Request $request): ?Tenant.

Rationale:

  • The spec explicitly forbids an unspecified universal resolver.
  • Existing code already encodes the correct admin behavior in OperateHubShell.
  • Tenant-panel screens legitimately use Filament::getTenant() and Tenant::current() because the tenant is part of panel-native routing semantics.

Alternatives considered:

  • Build a single app-wide tenant resolver service: rejected because it would blur the admin-versus-tenant distinction the spec wants to preserve.
  • Convert all existing tenant reads to admin semantics: rejected because tenant-panel pages are intentionally panel-native.

Decision 2: Treat OperateHubShell as the canonical admin priority rule

Decision: Use OperateHubShell::activeEntitledTenant(Request $request): ?Tenant as the single admin source of truth and document its priority order as:

  1. entitled Filament tenant,
  2. entitled remembered workspace tenant,
  3. null when neither is valid.

Rationale:

  • OperateHubShell already resolves the conflict the spec describes.
  • It already checks entitlement before returning a tenant.
  • It already drives visible admin UI labels such as “Filtered by tenant” and tenant return affordances.

Alternatives considered:

  • Resolve directly from session in each page/resource: rejected because the bug class is inconsistent per-surface resolution.
  • Prefer remembered tenant over Filament tenant: rejected because existing admin code and the specs conflict scenario require Filament tenant to win.

Decision 3: Use alert delivery as the admin reference pattern

Decision: Use AlertDeliveryResource as the “already correct” admin reference pattern for query scoping, tenant filter defaults, and tenant filter option narrowing.

Rationale:

  • The resource already scopes by workspace and tenant entitlement.
  • It already uses OperateHubShell::activeEntitledTenant() for both query narrowing and filter default behavior.
  • Its record URL and read-only list structure already fit the features target class of admin monitoring flows.

Alternatives considered:

  • Use Operations as the reference pattern: rejected because Operations is one of the surfaces that still has an inconsistency in its KPI widget.
  • Use a tenant-panel resource as reference: rejected because the feature is specifically about admin-panel canonical behavior.

Decision 4: Treat Operations page and KPI widget as one remediation unit

Decision: Plan the Operations page shell and OperationsKpiHeader widget as one remediation slice.

Rationale:

  • Monitoring\Operations already uses OperateHubShell for header actions and table query narrowing.
  • OperationsKpiHeader still reads Filament::getTenant() directly, which can disagree with the page header and table when only remembered tenant context exists.
  • The spec requires visible tenant context, counts, widgets, filters, and links to resolve from the same canonical tenant.

Alternatives considered:

  • Fix only the page query: rejected because the current inconsistency class is specifically page shell versus widget mismatch.
  • Leave the widget workspace-wide while the table is tenant-scoped: rejected because that violates visible-scope parity.

Decision 5: Harden OperationRun filters against stale persisted state

Decision: Revalidate tenant-sensitive persisted filters against the current canonical admin tenant on every relevant request.

Rationale:

  • OperationRunResource persists filters in session.
  • The tenant filter default already uses OperateHubShell, but some other filter option builders still reference raw Filament::getTenant().
  • The spec explicitly calls out persisted tenant-sensitive filter state as a regression risk when switching tenants.

Alternatives considered:

  • Disable filter persistence entirely: rejected because persistence is a broader UX choice outside this feature.
  • Trust the persisted filter if it still parses: rejected because syntactic validity is not the same as tenant-scope validity.

Decision 6: Entra groups need record-resolution hardening, not just table scoping

Decision: Treat Entra groups as a list-plus-record-resolution problem. Scope must be aligned in table(), getEloquentQuery(), direct record/view access, and global search.

Rationale:

  • EntraGroupResource::table() narrows using Tenant::current().
  • EntraGroupResource::getEloquentQuery() currently returns the broader base query.
  • The resource has a view page, so direct record URLs can bypass table-only constraints.
  • The spec explicitly names Entra group record-resolution and search as in-scope remediation.

Alternatives considered:

  • Scope only the table query: rejected because the feature exists to fix list/detail/search parity.
  • Remove the view page: rejected because the spec focuses on safe access, not reducing product capability.

Decision 7: Use a panel-aware search rule or disable admin global search for Entra groups

Decision: Prefer a panel-aware scoped global-search query for Entra groups. If implementation cannot guarantee parity cheaply, disable global search for that resource in admin contexts.

Rationale:

  • The constitution and spec both require tenant-safe, non-member-safe search behavior.
  • The repo already has a tenant-scoped search concern based on Filament::getTenant(), but that is not sufficient for admin remembered-tenant semantics.
  • The spec allows disabling global search where safe parity cannot be guaranteed.

Alternatives considered:

  • Leave current search behavior implicit: rejected because the spec explicitly calls out search entry points.
  • Build a broad search-only exception: rejected because it would reintroduce scope drift.

Decision 8: The guardrail should be a focused architecture test, not a new lint tool

Decision: Implement the lightweight guardrail as a Pest architecture/regression test that scans selected admin Filament paths for raw Filament::getTenant() and Tenant::current() patterns, with a documented allowlist for tenant-panel-native files.

Rationale:

  • The repo already relies on Pest for regression guards.
  • This keeps the feature self-contained and avoids adding external tooling.
  • The test can fail with actionable file paths and still permit explicit documented exceptions.

Alternatives considered:

  • Add a custom PHPStan rule or external linter: rejected because it is heavier than the spec requires.
  • Rely on code review discipline only: rejected because the feature explicitly asks for a maintainable guardrail.