085-tenant-operate-hub #104

Merged
ahmido merged 8 commits from 085-tenant-operate-hub into dev 2026-02-11 21:01:25 +00:00
6 changed files with 151 additions and 0 deletions

View File

@ -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 [

View File

@ -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();

View File

@ -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)

View File

@ -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)

View File

@ -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;
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Models\TenantMembership;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('redirects tenant-scoped admin surfaces to choose-tenant when no tenant is selected', function (): void {
$user = User::factory()->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');
});