From 98c7408c000bae3b44d7b004e29990c3d74f95a2 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Wed, 11 Feb 2026 21:59:56 +0100 Subject: [PATCH] fix(ui): hide tenant-scoped admin nav without tenant --- app/Filament/Pages/InventoryCoverage.php | 20 ++++++ app/Filament/Resources/BackupSetResource.php | 16 +++++ app/Filament/Resources/PolicyResource.php | 17 +++++ .../Resources/PolicyVersionResource.php | 16 +++++ .../EnsureFilamentTenantSelected.php | 17 +++++ ...opedSurfacesRedirectToChooseTenantTest.php | 65 +++++++++++++++++++ 6 files changed, 151 insertions(+) create mode 100644 tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseTenantTest.php diff --git a/app/Filament/Pages/InventoryCoverage.php b/app/Filament/Pages/InventoryCoverage.php index 0fff310..9bada7b 100644 --- a/app/Filament/Pages/InventoryCoverage.php +++ b/app/Filament/Pages/InventoryCoverage.php @@ -4,7 +4,11 @@ use App\Filament\Clusters\Inventory\InventoryCluster; use App\Filament\Widgets\Inventory\InventoryKpiHeader; +use App\Models\Tenant; +use App\Models\User; +use App\Services\Auth\CapabilityResolver; use App\Services\Inventory\CoverageCapabilitiesResolver; +use App\Support\Auth\Capabilities; use App\Support\Inventory\InventoryPolicyTypeMeta; use BackedEnum; use Filament\Pages\Page; @@ -24,6 +28,22 @@ class InventoryCoverage extends Page protected string $view = 'filament.pages.inventory-coverage'; + public static function canAccess(): bool + { + $tenant = Tenant::current(); + $user = auth()->user(); + + if (! $tenant instanceof Tenant || ! $user instanceof User) { + return false; + } + + /** @var CapabilityResolver $resolver */ + $resolver = app(CapabilityResolver::class); + + return $resolver->isMember($user, $tenant) + && $resolver->can($user, $tenant, Capabilities::TENANT_VIEW); + } + protected function getHeaderWidgets(): array { return [ diff --git a/app/Filament/Resources/BackupSetResource.php b/app/Filament/Resources/BackupSetResource.php index c64e6f0..8f26d7f 100644 --- a/app/Filament/Resources/BackupSetResource.php +++ b/app/Filament/Resources/BackupSetResource.php @@ -49,6 +49,22 @@ class BackupSetResource extends Resource protected static string|UnitEnum|null $navigationGroup = 'Backups & Restore'; + public static function canViewAny(): bool + { + $tenant = Tenant::current(); + $user = auth()->user(); + + if (! $tenant instanceof Tenant || ! $user instanceof User) { + return false; + } + + /** @var CapabilityResolver $resolver */ + $resolver = app(CapabilityResolver::class); + + return $resolver->isMember($user, $tenant) + && $resolver->can($user, $tenant, Capabilities::TENANT_VIEW); + } + public static function canCreate(): bool { $tenant = Tenant::current(); diff --git a/app/Filament/Resources/PolicyResource.php b/app/Filament/Resources/PolicyResource.php index 86cd290..a666d1a 100644 --- a/app/Filament/Resources/PolicyResource.php +++ b/app/Filament/Resources/PolicyResource.php @@ -11,6 +11,7 @@ use App\Models\Policy; use App\Models\Tenant; use App\Models\User; +use App\Services\Auth\CapabilityResolver; use App\Services\Intune\PolicyNormalizer; use App\Services\OperationRunService; use App\Services\Operations\BulkSelectionIdentity; @@ -58,6 +59,22 @@ class PolicyResource extends Resource protected static string|UnitEnum|null $navigationGroup = 'Inventory'; + public static function canViewAny(): bool + { + $tenant = Tenant::current(); + $user = auth()->user(); + + if (! $tenant instanceof Tenant || ! $user instanceof User) { + return false; + } + + /** @var CapabilityResolver $resolver */ + $resolver = app(CapabilityResolver::class); + + return $resolver->isMember($user, $tenant) + && $resolver->can($user, $tenant, Capabilities::TENANT_VIEW); + } + public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) diff --git a/app/Filament/Resources/PolicyVersionResource.php b/app/Filament/Resources/PolicyVersionResource.php index b2d2234..4ff27d1 100644 --- a/app/Filament/Resources/PolicyVersionResource.php +++ b/app/Filament/Resources/PolicyVersionResource.php @@ -57,6 +57,22 @@ class PolicyVersionResource extends Resource protected static string|UnitEnum|null $navigationGroup = 'Inventory'; + public static function canViewAny(): bool + { + $tenant = Tenant::current(); + $user = auth()->user(); + + if (! $tenant instanceof Tenant || ! $user instanceof User) { + return false; + } + + /** @var CapabilityResolver $resolver */ + $resolver = app(CapabilityResolver::class); + + return $resolver->isMember($user, $tenant) + && $resolver->can($user, $tenant, Capabilities::TENANT_VIEW); + } + public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) diff --git a/app/Support/Middleware/EnsureFilamentTenantSelected.php b/app/Support/Middleware/EnsureFilamentTenantSelected.php index a8d04f3..fff692a 100644 --- a/app/Support/Middleware/EnsureFilamentTenantSelected.php +++ b/app/Support/Middleware/EnsureFilamentTenantSelected.php @@ -72,6 +72,14 @@ public function handle(Request $request, Closure $next): Response $tenantParameter = $request->query('tenant'); } + if ( + $tenantParameter === null + && ! filled(Filament::getTenant()) + && $this->adminPathRequiresTenantSelection($path) + ) { + return redirect()->route('filament.admin.pages.choose-tenant'); + } + if ($tenantParameter !== null) { $user = $request->user(); @@ -208,4 +216,13 @@ private function configureNavigationForRequest(\Filament\Panel $panel): void ); }); } + + private function adminPathRequiresTenantSelection(string $path): bool + { + if (! str_starts_with($path, '/admin/')) { + return false; + } + + return preg_match('#^/admin/(inventory|policies|policy-versions|backup-sets)(/|$)#', $path) === 1; + } } diff --git a/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseTenantTest.php b/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseTenantTest.php new file mode 100644 index 0000000..15a95a6 --- /dev/null +++ b/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseTenantTest.php @@ -0,0 +1,65 @@ +create(); + + $workspace = Workspace::factory()->create(); + + WorkspaceMembership::factory()->create([ + 'workspace_id' => $workspace->getKey(), + 'user_id' => $user->getKey(), + 'role' => 'owner', + ]); + + $tenants = Tenant::factory()->count(2)->create([ + 'status' => 'active', + 'workspace_id' => $workspace->getKey(), + ]); + + foreach ($tenants as $tenant) { + TenantMembership::query()->create([ + 'tenant_id' => $tenant->getKey(), + 'user_id' => $user->getKey(), + 'role' => 'owner', + 'source' => 'manual', + 'source_ref' => null, + 'created_by_user_id' => null, + ]); + } + + $this + ->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) + ->get('/admin/policies') + ->assertRedirect('/admin/choose-tenant'); + + $this + ->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) + ->get('/admin/policy-versions') + ->assertRedirect('/admin/choose-tenant'); + + $this + ->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) + ->get('/admin/backup-sets') + ->assertRedirect('/admin/choose-tenant'); + + $this + ->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) + ->get('/admin/inventory') + ->assertRedirect('/admin/choose-tenant'); +});