## Summary - retire legacy `/admin/t` and active `/admin/tenants` product surfaces in favor of canonical workspace-scoped managed-environment routes - centralize runtime URL generation through `ManagedEnvironmentLinks` and update intended URL handling to reject legacy tenant paths - remove dormant tenant panel runtime, rename test helpers to the admin environment context, and add guard coverage for route/helper regressions ## Validation - targeted Feature guard, workspace, provider connection, required permissions, and Filament test lanes run under Sail - browser smoke coverage run for provider connection and workspace RBAC environment access flows - formatting and diff checks completed with Pint and `git diff --check` ## Notes - Filament remains on v5 with Livewire v4 - provider registration stays in `apps/platform/bootstrap/providers.php` - retired tenant resource global search is disabled and destructive action confirmation rules remain unchanged Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #352
279 lines
11 KiB
PHP
279 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Services\Auth\TenantMembershipManager;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\ManagedEnvironmentLinks;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
Http::preventStrayRequests();
|
|
});
|
|
|
|
it('allows workspace members to open the canonical workspace-managed environments index', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(ManagedEnvironmentLinks::indexUrl($tenant))
|
|
->assertOk();
|
|
});
|
|
|
|
it('returns 404 for non-members on the workspace-managed tenants index', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get('/admin/tenants')
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('allows workspace members to open the canonical workspace-managed environment view route', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(ManagedEnvironmentLinks::viewUrl($tenant))
|
|
->assertOk();
|
|
});
|
|
|
|
it('exposes a canonical provider connections link for a managed environment', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(ManagedEnvironmentLinks::viewUrl($tenant))
|
|
->assertOk();
|
|
|
|
expect(ManagedEnvironmentLinks::providerConnectionsUrl($tenant))
|
|
->toContain('/admin/provider-connections?managed_environment_id='.$tenant->external_id)
|
|
->not->toContain('/admin/tenants')
|
|
->not->toContain('/admin/t/');
|
|
});
|
|
|
|
it('returns 404 for non-members on the workspace-managed tenant view route', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get("/admin/tenants/{$tenant->external_id}")
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('exposes access-scope management under workspace scope', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(ManagedEnvironmentLinks::accessScopesUrl($tenant))
|
|
->assertOk();
|
|
});
|
|
|
|
it('keeps retired tenant panel operational routes unavailable even for entitled workspace members', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
$tenant = ManagedEnvironment::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'external_id' => '11111111-1111-1111-1111-111111111111',
|
|
'managed_environment_id' => '11111111-1111-1111-1111-111111111111',
|
|
]);
|
|
|
|
[$entitledUser] = createMinimalUserWithTenant(tenant: $tenant, role: 'readonly');
|
|
|
|
$nonEntitledUser = User::factory()->create();
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $nonEntitledUser->getKey(),
|
|
'role' => 'owner',
|
|
]);
|
|
|
|
$this->actingAs($entitledUser)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
|
->get("/admin/t/{$tenant->external_id}")
|
|
->assertNotFound();
|
|
|
|
$this->actingAs($entitledUser)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
|
->get("/admin/t/{$tenant->external_id}/diagnostics")
|
|
->assertNotFound();
|
|
|
|
$this->actingAs($nonEntitledUser)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
|
->get("/admin/t/{$tenant->external_id}")
|
|
->assertNotFound();
|
|
|
|
$this->actingAs($nonEntitledUser)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
|
->get("/admin/t/{$tenant->external_id}/diagnostics")
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('keeps retired tenant panel route shapes unavailable and rejects duplicated /t prefixes', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get("/admin/t/{$tenant->external_id}/diagnostics")
|
|
->assertNotFound();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get("/admin/t/t/{$tenant->external_id}/diagnostics")
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('removes tenant-scoped management routes', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get("/admin/t/{$tenant->external_id}/provider-connections")
|
|
->assertNotFound();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
|
->assertNotFound();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get("/admin/t/{$tenant->external_id}/memberships")
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('serves provider connection management under canonical admin routes only', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$connection = ProviderConnection::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
]);
|
|
|
|
$this->followingRedirects()
|
|
->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get('/admin/provider-connections?managed_environment_id='.$tenant->external_id)
|
|
->assertOk();
|
|
|
|
$this->followingRedirects()
|
|
->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get('/admin/provider-connections/'.$connection->getKey().'/edit?managed_environment_id='.$tenant->external_id)
|
|
->assertOk();
|
|
});
|
|
|
|
it('returns 403 for workspace members missing mutation capability on canonical provider connection routes', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'readonly', workspaceRole: 'readonly');
|
|
|
|
$this->followingRedirects()
|
|
->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get('/admin/provider-connections?managed_environment_id='.$tenant->external_id)
|
|
->assertOk();
|
|
|
|
$this->followingRedirects()
|
|
->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get('/admin/provider-connections/create?managed_environment_id='.$tenant->external_id)
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('writes managed-environment access scope audit entries for scope mutations', function (): void {
|
|
[$owner, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
$member = User::factory()->create();
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => (int) $member->getKey(),
|
|
'role' => 'readonly',
|
|
]);
|
|
|
|
/** @var TenantMembershipManager $manager */
|
|
$manager = app(TenantMembershipManager::class);
|
|
|
|
$membership = $manager->addMember(
|
|
tenant: $tenant,
|
|
actor: $owner,
|
|
member: $member,
|
|
role: 'readonly',
|
|
source: 'manual',
|
|
);
|
|
|
|
$manager->removeMember(
|
|
tenant: $tenant,
|
|
actor: $owner,
|
|
membership: $membership,
|
|
);
|
|
|
|
$actions = AuditLog::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->whereIn('action', [
|
|
AuditActionId::ManagedEnvironmentAccessScopeGrant->value,
|
|
AuditActionId::ManagedEnvironmentAccessScopeRemove->value,
|
|
])
|
|
->pluck('action')
|
|
->all();
|
|
|
|
expect($actions)->toContain(AuditActionId::ManagedEnvironmentAccessScopeGrant->value)
|
|
->and($actions)->toContain(AuditActionId::ManagedEnvironmentAccessScopeRemove->value)
|
|
->and($actions)->not->toContain(AuditActionId::TenantMembershipRoleChange->value);
|
|
});
|
|
|
|
it('keeps the canonical managed-environment index available after panel split', function (): void {
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(ManagedEnvironmentLinks::indexUrl($tenant))
|
|
->assertOk()
|
|
->assertSee('Managed environments')
|
|
->assertDontSee('/admin/tenants', false)
|
|
->assertDontSee('/admin/t/', false);
|
|
});
|
|
|
|
it('does not expose tenant-management resources in tenant panel registration or navigation URLs', function (): void {
|
|
expect(Filament::getPanel('tenant'))->toBeNull();
|
|
|
|
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(ManagedEnvironmentLinks::viewUrl($tenant))
|
|
->assertOk()
|
|
->assertDontSee("/admin/t/{$tenant->external_id}/provider-connections", false)
|
|
->assertDontSee("/admin/t/{$tenant->external_id}/tenants", false);
|
|
});
|
|
|
|
it('disables global search on the retired TenantResource product route owner', function (): void {
|
|
[$workspaceUser, $tenant] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
Filament::setCurrentPanel('admin');
|
|
Filament::setTenant(null, true);
|
|
|
|
$this->actingAs($workspaceUser);
|
|
|
|
expect(\App\Filament\Resources\TenantResource::getGlobalSearchResults((string) $tenant->name))->toHaveCount(0);
|
|
|
|
$nonMember = User::factory()->create();
|
|
|
|
Filament::setCurrentPanel('admin');
|
|
Filament::setTenant(null, true);
|
|
|
|
$this->actingAs($nonMember);
|
|
|
|
expect(\App\Filament\Resources\TenantResource::getGlobalSearchResults((string) $tenant->name))->toHaveCount(0);
|
|
});
|