feat: canonicalize admin scope links and queries (341) #413
1
.gitignore
vendored
1
.gitignore
vendored
@ -56,6 +56,7 @@ Thumbs.db
|
||||
/references
|
||||
/tests/Browser/Screenshots
|
||||
/apps/platform/tests/Browser/Screenshots
|
||||
/.playwright-mcp
|
||||
*.tmp
|
||||
*.swp
|
||||
/apps/platform/.env
|
||||
|
||||
@ -247,14 +247,6 @@ protected static function resolveScopedTenant(ManagedEnvironment|string|null $te
|
||||
->first();
|
||||
}
|
||||
|
||||
$queryTenant = request()->query('tenant');
|
||||
|
||||
if (is_string($queryTenant) && $queryTenant !== '') {
|
||||
return ManagedEnvironment::query()
|
||||
->where('slug', $queryTenant)
|
||||
->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -332,6 +332,11 @@ public function table(Table $table): Table
|
||||
->visible(fn (): bool => $this->registerState === 'recently_closed')
|
||||
->wrap(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('managed_environment_id')
|
||||
->label('Environment')
|
||||
->options(fn (): array => $this->tenantFilterOptions()),
|
||||
])
|
||||
->emptyStateHeading($this->emptyStateHeading())
|
||||
->emptyStateDescription($this->emptyStateDescription())
|
||||
->emptyStateActions($this->emptyStateActions());
|
||||
@ -476,6 +481,21 @@ private function visibleDecisionTenants(): array
|
||||
return $this->visibleDecisionTenants = static::resolveVisibleDecisionTenantsFor($user, $workspace, $tenants);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function tenantFilterOptions(): array
|
||||
{
|
||||
$options = [];
|
||||
|
||||
foreach ($this->visibleDecisionTenants() as $tenant) {
|
||||
$label = (string) ($tenant->name ?: $tenant->slug ?: ('Environment '.(int) $tenant->getKey()));
|
||||
$options[(string) $tenant->getKey()] = $label;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function applyRequestedTenantPrefilter(): void
|
||||
{
|
||||
$workspace = $this->workspace();
|
||||
@ -495,6 +515,8 @@ private function applyRequestedTenantPrefilter(): void
|
||||
foreach ($this->visibleDecisionTenants() as $tenant) {
|
||||
if ((int) $tenant->getKey() === $environmentId) {
|
||||
$this->tenantId = $environmentId;
|
||||
$this->tableFilters['managed_environment_id']['value'] = (string) $environmentId;
|
||||
$this->tableDeferredFilters['managed_environment_id']['value'] = (string) $environmentId;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -289,7 +289,7 @@ public function table(Table $table): Table
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
TextColumn::make('tenant.name')
|
||||
->label('ManagedEnvironment')
|
||||
->label('Environment')
|
||||
->formatStateUsing(fn (?string $state): string => $state ?: 'Workspace')
|
||||
->toggleable(),
|
||||
TextColumn::make('recorded_at')
|
||||
@ -299,7 +299,7 @@ public function table(Table $table): Table
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('managed_environment_id')
|
||||
->label('ManagedEnvironment')
|
||||
->label('Environment')
|
||||
->options(fn (): array => $this->tenantFilterOptions())
|
||||
->default(fn (): ?string => $this->defaultTenantFilter())
|
||||
->searchable(),
|
||||
|
||||
@ -185,7 +185,7 @@ public static function infolist(Schema $schema): Schema
|
||||
->formatStateUsing(fn (?string $state): string => ucfirst((string) $state))
|
||||
->placeholder('—'),
|
||||
TextEntry::make('tenant.name')
|
||||
->label('ManagedEnvironment'),
|
||||
->label('Environment'),
|
||||
TextEntry::make('rule.name')
|
||||
->label('Rule')
|
||||
->placeholder('—'),
|
||||
@ -246,7 +246,7 @@ public static function table(Table $table): Table
|
||||
->since()
|
||||
->sortable(),
|
||||
TextColumn::make('tenant.name')
|
||||
->label('ManagedEnvironment')
|
||||
->label('Environment')
|
||||
->searchable(),
|
||||
TextColumn::make('event_type')
|
||||
->label('Event')
|
||||
@ -274,7 +274,7 @@ public static function table(Table $table): Table
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('managed_environment_id')
|
||||
->label('ManagedEnvironment')
|
||||
->label('Environment')
|
||||
->options(function (): array {
|
||||
$user = auth()->user();
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\CapabilityResolver;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Schemas\Components\EmbeddedTable;
|
||||
@ -28,6 +29,7 @@ public function mount(): void
|
||||
|
||||
parent::mount();
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
$this->applyRequestedEnvironmentFilter();
|
||||
}
|
||||
|
||||
private function tableHasRecords(): bool
|
||||
@ -243,6 +245,34 @@ private function resolveTenantExternalIdForCreateAction(): ?string
|
||||
return null;
|
||||
}
|
||||
|
||||
private function applyRequestedEnvironmentFilter(): void
|
||||
{
|
||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
||||
|
||||
if (! is_int($workspaceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$environment = ProviderConnectionResource::resolveRequestedEnvironment();
|
||||
|
||||
if (! $environment instanceof ManagedEnvironment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int) $environment->workspace_id !== $workspaceId) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$slug = (string) $environment->slug;
|
||||
|
||||
if ($slug === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tableFilters['tenant']['value'] = $slug;
|
||||
$this->tableDeferredFilters['tenant']['value'] = $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{label: string, clear_url: string}|null
|
||||
*/
|
||||
|
||||
@ -214,10 +214,6 @@ private function requestHasExplicitTenantContext(Request $request): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filled($request->query('tenant')) || filled($request->query('managed_environment_id'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$route = $request->route();
|
||||
|
||||
return $route?->hasParameter('tenant') && filled($route->parameter('tenant'));
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
use App\Models\User;
|
||||
use App\Support\Navigation\AdminSurfaceScope;
|
||||
use App\Support\Navigation\NavigationScope;
|
||||
use App\Support\Navigation\WorkspaceHubRegistry;
|
||||
use App\Support\Navigation\WorkspaceSidebarNavigation;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
@ -87,10 +86,6 @@ public function handle(Request $request, Closure $next): Response
|
||||
! $resolvedContext->hasTenant()
|
||||
&& $this->adminPathRequiresTenantSelection($path)
|
||||
) {
|
||||
if ($this->requestHasExplicitTenantHint($request)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$workspace = $workspaceContext->currentWorkspace($request);
|
||||
|
||||
if ($workspace !== null) {
|
||||
@ -178,15 +173,6 @@ private function isCanonicalWorkspaceRecordViewerPath(string $path): bool
|
||||
return AdminSurfaceScope::fromPath($path) === AdminSurfaceScope::CanonicalWorkspaceRecordViewer;
|
||||
}
|
||||
|
||||
private function requestHasExplicitTenantHint(Request $request): bool
|
||||
{
|
||||
if (WorkspaceHubRegistry::isWorkspaceHubPath('/'.ltrim((string) $request->path(), '/'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filled($request->query('tenant')) || filled($request->query('managed_environment_id'));
|
||||
}
|
||||
|
||||
private function adminPathRequiresTenantSelection(string $path): bool
|
||||
{
|
||||
if (! str_starts_with($path, '/admin/')) {
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
namespace App\Support\Navigation;
|
||||
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
|
||||
@ -50,6 +51,12 @@ public static function environmentContext(): ?ManagedEnvironment
|
||||
return null;
|
||||
}
|
||||
|
||||
$resolved = app(OperateHubShell::class)->resolvedContext(request());
|
||||
|
||||
if ($resolved->tenant instanceof ManagedEnvironment) {
|
||||
return $resolved->tenant;
|
||||
}
|
||||
|
||||
$tenant = Filament::getTenant();
|
||||
|
||||
if ($tenant instanceof ManagedEnvironment) {
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
use App\Services\Tenants\TenantOperabilityService;
|
||||
use App\Support\ManagedEnvironmentLinks;
|
||||
use App\Support\Navigation\AdminSurfaceScope;
|
||||
use App\Support\Navigation\WorkspaceHubRegistry;
|
||||
use App\Support\Tenants\TenantOperabilityQuestion;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Actions\Action;
|
||||
@ -185,35 +184,6 @@ private function buildResolvedContext(?Request $request = null): ResolvedShellCo
|
||||
);
|
||||
}
|
||||
|
||||
$queryHintTenant = $this->resolveValidatedQueryHintTenant($request, $workspace, $pageCategory);
|
||||
|
||||
if ($queryHintTenant['tenant'] instanceof ManagedEnvironment) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: $queryHintTenant['tenant'],
|
||||
pageCategory: $pageCategory,
|
||||
state: 'tenant_scoped',
|
||||
displayMode: 'tenant_scoped',
|
||||
workspaceSource: $workspaceSource,
|
||||
tenantSource: 'query_hint',
|
||||
);
|
||||
}
|
||||
|
||||
$recoveryReason ??= $queryHintTenant['reason'];
|
||||
|
||||
if ($this->requiresStrictQueryTenantHintResolution($request)) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: null,
|
||||
pageCategory: $pageCategory,
|
||||
state: 'invalid_tenant',
|
||||
displayMode: 'recovery',
|
||||
workspaceSource: $workspaceSource,
|
||||
recoveryAction: 'abort_not_found',
|
||||
recoveryReason: $recoveryReason ?? 'missing_tenant',
|
||||
);
|
||||
}
|
||||
|
||||
$tenant = $this->resolveValidatedFilamentTenant($request, $pageCategory, $workspace);
|
||||
|
||||
if ($tenant instanceof ManagedEnvironment) {
|
||||
@ -324,30 +294,6 @@ private function resolveWorkspaceForPageCategory(
|
||||
};
|
||||
}
|
||||
|
||||
private function resolveValidatedQueryHintTenant(
|
||||
?Request $request,
|
||||
Workspace $workspace,
|
||||
AdminSurfaceScope $pageCategory,
|
||||
): array {
|
||||
if (! $pageCategory->allowsQueryEnvironmentHints()) {
|
||||
return ['tenant' => null, 'reason' => null];
|
||||
}
|
||||
|
||||
$queryTenant = $this->resolveQueryTenantHint($request);
|
||||
|
||||
if (! $queryTenant instanceof ManagedEnvironment) {
|
||||
return ['tenant' => null, 'reason' => null];
|
||||
}
|
||||
|
||||
$reason = $this->tenantValidationReason($queryTenant, $workspace, $request, $pageCategory);
|
||||
|
||||
if ($reason !== null) {
|
||||
return ['tenant' => null, 'reason' => $reason];
|
||||
}
|
||||
|
||||
return ['tenant' => $queryTenant, 'reason' => null];
|
||||
}
|
||||
|
||||
private function resolveRouteTenantCandidate(?Request $request = null, ?AdminSurfaceScope $pageCategory = null): ?ManagedEnvironment
|
||||
{
|
||||
$route = $request?->route();
|
||||
@ -412,55 +358,6 @@ private function resolveRefererTenantCandidate(?Request $request, AdminSurfaceSc
|
||||
return $this->resolveTenantIdentifier($environmentIdentifier);
|
||||
}
|
||||
|
||||
private function resolveQueryTenantHint(?Request $request = null): ?ManagedEnvironment
|
||||
{
|
||||
if ($request instanceof Request && WorkspaceHubRegistry::isWorkspaceHubPath('/'.ltrim((string) $request->path(), '/'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$queryTenant = $request?->query('tenant');
|
||||
|
||||
if (filled($queryTenant)) {
|
||||
return $this->resolveTenantIdentifier($queryTenant);
|
||||
}
|
||||
|
||||
$queryTenantId = $request?->query('managed_environment_id');
|
||||
|
||||
if (filled($queryTenantId)) {
|
||||
return $this->resolveTenantIdentifier($queryTenantId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function hasExplicitQueryTenantHint(?Request $request = null): bool
|
||||
{
|
||||
if ($request instanceof Request && WorkspaceHubRegistry::isWorkspaceHubPath('/'.ltrim((string) $request->path(), '/'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filled($request?->query('tenant')) || filled($request?->query('managed_environment_id'));
|
||||
}
|
||||
|
||||
private function requiresStrictQueryTenantHintResolution(?Request $request = null): bool
|
||||
{
|
||||
if (! $this->hasExplicitQueryTenantHint($request)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = '/'.ltrim((string) $request?->path(), '/');
|
||||
|
||||
if (! str_starts_with($path, '/admin/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_starts_with($path, '/admin/finding-exceptions/queue')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match('#^/admin/(inventory|policies|policy-versions|backup-sets|backup-schedules|findings|finding-exceptions)(/|$)#', $path) === 1;
|
||||
}
|
||||
|
||||
private function resolveTenantIdentifier(mixed $tenantIdentifier): ?ManagedEnvironment
|
||||
{
|
||||
if ($tenantIdentifier instanceof ManagedEnvironment) {
|
||||
|
||||
@ -110,7 +110,7 @@
|
||||
$query->where('slug', $identifier);
|
||||
|
||||
if (ctype_digit($identifier)) {
|
||||
$query->orWhereKey((int) $identifier);
|
||||
$query->orWhere('id', (int) $identifier);
|
||||
}
|
||||
})
|
||||
->first();
|
||||
@ -132,7 +132,7 @@
|
||||
$query->where('slug', $identifier);
|
||||
|
||||
if (ctype_digit($identifier)) {
|
||||
$query->orWhereKey((int) $identifier);
|
||||
$query->orWhere('id', (int) $identifier);
|
||||
}
|
||||
})
|
||||
->first();
|
||||
|
||||
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\EnvironmentRequiredPermissions;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Support\ManagedEnvironmentLinks;
|
||||
use App\Support\Navigation\WorkspaceHubNavigation;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
it('Spec341 OperateHubShell ignores legacy tenant query hints', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec341 Environment A',
|
||||
'external_id' => 'spec341-environment-a',
|
||||
]);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
|
||||
$workspace = $environment->workspace()->firstOrFail();
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
||||
session()->forget(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY);
|
||||
|
||||
$shell = app(OperateHubShell::class);
|
||||
|
||||
$queryCases = [
|
||||
'tenant' => ['tenant' => (string) $environment->getRouteKey()],
|
||||
'managed_environment_id' => ['managed_environment_id' => (int) $environment->getKey()],
|
||||
];
|
||||
|
||||
foreach ($queryCases as $query) {
|
||||
$request = Request::create('/admin/onboarding', 'GET', $query);
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
$resolved = $shell->resolvedContext($request);
|
||||
|
||||
expect($resolved->hasTenant())->toBeFalse()
|
||||
->and($resolved->tenantSource)->not->toBe('query_hint');
|
||||
}
|
||||
});
|
||||
|
||||
it('Spec341 EnvironmentRequiredPermissions does not resolve tenant from legacy query keys', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec341 Permissions Environment',
|
||||
'external_id' => 'spec341-permissions-environment',
|
||||
]);
|
||||
|
||||
$originalRequest = app('request');
|
||||
|
||||
try {
|
||||
$request = Request::create('/livewire/update', 'GET', [
|
||||
'tenant' => (string) $environment->getRouteKey(),
|
||||
]);
|
||||
app()->instance('request', $request);
|
||||
|
||||
$method = new ReflectionMethod(EnvironmentRequiredPermissions::class, 'resolveScopedTenant');
|
||||
$method->setAccessible(true);
|
||||
|
||||
/** @var ManagedEnvironment|null $resolvedTenant */
|
||||
$resolvedTenant = $method->invoke(null, null);
|
||||
|
||||
expect($resolvedTenant)->toBeNull();
|
||||
} finally {
|
||||
app()->instance('request', $originalRequest);
|
||||
}
|
||||
});
|
||||
|
||||
it('Spec341 environment-bound routes remain route-owned even when legacy tenant query hints are present', function (): void {
|
||||
$environmentA = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec341 Baseline Environment A',
|
||||
'external_id' => 'spec341-baseline-environment-a',
|
||||
]);
|
||||
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$environmentB = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'name' => 'Spec341 Baseline Environment B',
|
||||
'external_id' => 'spec341-baseline-environment-b',
|
||||
]);
|
||||
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
|
||||
|
||||
baselineCompareLandingLivewire($environmentB, ['tenant' => (string) $environmentA->getRouteKey()], $user)
|
||||
->assertSet('scopedEnvironmentId', (int) $environmentB->getKey());
|
||||
});
|
||||
|
||||
it('Spec341 WorkspaceHubNavigation carries route-owned environment context into workspace hub URLs', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec341 Hub Navigation Environment',
|
||||
'external_id' => 'spec341-hub-navigation-environment',
|
||||
]);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
|
||||
$workspace = $environment->workspace()->firstOrFail();
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
|
||||
Filament::setTenant(null, true);
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
||||
session()->forget(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY);
|
||||
|
||||
$originalRequest = app('request');
|
||||
|
||||
try {
|
||||
$workspaceKey = ManagedEnvironmentLinks::workspaceRouteKey($workspace);
|
||||
$environmentKey = ManagedEnvironmentLinks::environmentRouteKey($environment);
|
||||
|
||||
$request = Request::create('/livewire/update', 'POST');
|
||||
$request->headers->set('x-livewire', '1');
|
||||
$request->headers->set('referer', url("/admin/workspaces/{$workspaceKey}/environments/{$environmentKey}/required-permissions"));
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
app()->instance('request', $request);
|
||||
|
||||
$url = WorkspaceHubNavigation::environmentFilteredUrl(url('/admin/provider-connections'));
|
||||
|
||||
$query = [];
|
||||
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
|
||||
|
||||
expect((int) ($query['environment_id'] ?? 0))->toBe((int) $environment->getKey());
|
||||
} finally {
|
||||
app()->instance('request', $originalRequest);
|
||||
}
|
||||
});
|
||||
@ -110,6 +110,12 @@
|
||||
->assertSee($environmentA->name)
|
||||
->assertDontSee('Spec315 Decision B');
|
||||
|
||||
Livewire::withQueryParams(['environment_id' => (int) $environmentA->getKey()])
|
||||
->actingAs($user)
|
||||
->test(DecisionRegister::class)
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $environmentA->getKey())
|
||||
->assertSet('tableDeferredFilters.managed_environment_id.value', (string) $environmentA->getKey());
|
||||
|
||||
$this->get(route('admin.evidence.overview', ['environment_id' => (int) $environmentA->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Environment filter:')
|
||||
|
||||
@ -116,3 +116,26 @@
|
||||
|
||||
expect(data_get(session()->get($filtersSessionKey, []), 'tenant.value'))->toBeNull();
|
||||
});
|
||||
|
||||
it('Spec341 provider connections translates environment_id query into an active table filter', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'external_id' => 'spec341-provider-connections-env',
|
||||
]);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
|
||||
|
||||
ProviderConnection::factory()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'display_name' => 'Spec341 Provider Filtered',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->withQueryParams(['environment_id' => (int) $environment->getKey()])
|
||||
->test(ListProviderConnections::class)
|
||||
->assertSet('tableFilters.tenant.value', (string) $environment->slug)
|
||||
->assertSet('tableDeferredFilters.tenant.value', (string) $environment->slug);
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
@ -0,0 +1,48 @@
|
||||
# Specification Quality Checklist: Spec 341 - Canonical Link / Query Cleanup
|
||||
|
||||
**Purpose**: Validate Spec 341 preparation completeness before implementation.
|
||||
**Created**: 2026-05-31
|
||||
**Feature**: `specs/341-canonical-link-query-cleanup/spec.md`
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- [x] CHK001 The selected candidate matches `canonical-link-query-cleanup` in `docs/product/spec-candidates.md`.
|
||||
- [x] CHK002 Related specs were checked for completed-spec signals and are treated as context only (no rewrites).
|
||||
- [x] CHK003 The package does not overwrite an existing `specs/341-*` directory.
|
||||
- [x] CHK004 Close alternatives (`environment-resource-context-follow-through`, `product-truth-docs-drift-cleanup`) are deferred instead of hidden scope.
|
||||
- [x] CHK005 The spec is scoped as link/query contract hygiene (no feature expansion).
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] CHK006 The spec has a concrete problem statement, user value, and explicit non-goals.
|
||||
- [x] CHK007 The spec includes acceptance criteria, out-of-scope boundaries, assumptions, risks, and open questions.
|
||||
- [x] CHK008 The spec avoids new persistence, new abstractions, or frameworking without proportionality justification.
|
||||
- [x] CHK009 No placeholder markers (`[FEATURE]`, `[DATE]`, `NEEDS CLARIFICATION`) remain in `spec.md`, `plan.md`, or `tasks.md`.
|
||||
- [x] CHK010 The spec’s scope is small enough for a bounded implementation loop.
|
||||
|
||||
## Constitution And Scope
|
||||
|
||||
- [x] CHK011 The Spec Candidate Check is filled and scored above the approval threshold.
|
||||
- [x] CHK012 Pre-production posture is respected: no compatibility redirects or legacy alias preservation “just in case”.
|
||||
- [x] CHK013 Workspace/environment isolation and deny-as-not-found semantics are explicitly preserved.
|
||||
- [x] CHK014 UI/Productization Coverage is completed as “no new surface; contract hardening only”.
|
||||
|
||||
## Plan Quality
|
||||
|
||||
- [x] CHK015 `plan.md` is repo-aware and lists the expected touched seams.
|
||||
- [x] CHK016 The plan sequences work as inventory → failing tests → behavior change → regression guards → validation.
|
||||
- [x] CHK017 Test governance is explicit: Feature lane first; browser only if justified.
|
||||
- [x] CHK018 Deployment impact is explicitly none (no migrations/env/queues/assets).
|
||||
|
||||
## Task Quality
|
||||
|
||||
- [x] CHK019 `tasks.md` exists and is dependency-ordered into small phases.
|
||||
- [x] CHK020 Tasks reference concrete repo files and do not invent new architecture.
|
||||
- [x] CHK021 Tasks include explicit validation commands and formatting checks.
|
||||
- [x] CHK022 Tasks explicitly forbid schema/persistence/framework scope expansion.
|
||||
|
||||
## Spec Readiness Gate
|
||||
|
||||
- [x] CHK023 `spec.md`, `plan.md`, and `tasks.md` exist.
|
||||
- [x] CHK024 No open question blocks safe implementation (inventory-driven follow-ups are allowed).
|
||||
- [x] CHK025 Result: ready for implementation loop.
|
||||
111
specs/341-canonical-link-query-cleanup/plan.md
Normal file
111
specs/341-canonical-link-query-cleanup/plan.md
Normal file
@ -0,0 +1,111 @@
|
||||
# Implementation Plan: Spec 341 - Canonical Link / Query Cleanup
|
||||
|
||||
**Branch**: `341-canonical-link-query-cleanup` | **Date**: 2026-05-31 | **Spec**: `specs/341-canonical-link-query-cleanup/spec.md`
|
||||
**Input**: Candidate `canonical-link-query-cleanup` from `docs/product/spec-candidates.md` + repo inspection of legacy scope query parsing seams.
|
||||
|
||||
## Summary
|
||||
|
||||
Inventory and remove remaining legacy “scope hint” query parsing (`tenant`, `managed_environment_id`, etc.) in shared request-scoping seams and align canonical link generation so that:
|
||||
|
||||
- workspace hubs are workspace-wide unless explicitly filtered via `environment_id`;
|
||||
- environment-bound pages are route-owned and do not accept legacy query aliases as authority; and
|
||||
- “clear filter” behavior returns to clean canonical URLs.
|
||||
|
||||
This is contract hardening; it must not introduce new abstractions, persistence, or UI frameworks.
|
||||
|
||||
## Technical Context
|
||||
|
||||
- **Language/Version**: PHP 8.4.15, Laravel 12.52.x
|
||||
- **Primary Dependencies**: Filament v5, Livewire v4, Pest v4
|
||||
- **Storage**: PostgreSQL (no schema changes planned)
|
||||
- **Testing**: Pest Feature tests (navigation + scope contracts)
|
||||
- **Validation Lanes**: fast-feedback (Feature); browser only if later proven necessary for visible UX behaviors that cannot be asserted reliably in Feature tests
|
||||
- **Target Platform**: `apps/platform` (Sail locally; Dokploy/container posture unchanged)
|
||||
- **Constraints**:
|
||||
- no compatibility redirects or “legacy alias preservation”
|
||||
- no new packages, migrations, queues, scheduler, storage changes
|
||||
- deny-as-not-found semantics remain authoritative
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: existing reachable surfaces; navigation + scope contract hardening
|
||||
- **Affected routes/pages/actions/states** (inventory-driven):
|
||||
- workspace hubs that accept environment narrowing via `environment_id` and expose “clear filter” behavior
|
||||
- environment-bound pages under `/admin/workspaces/{workspace}/environments/{environment}/...` that must remain route-owned
|
||||
- shared request-scoping seams that must not treat legacy query keys as authority
|
||||
- **State layers in scope**: URL/query + route parameters + session workspace context
|
||||
- **Handling mode**: review-mandatory (scope/authority semantics)
|
||||
- **Required tests or manual smoke**: Feature contract tests for clean vs filtered hub URLs and legacy alias rejection/ignore behavior; browser smoke only if required later
|
||||
- **UI/Productization coverage decision**: existing pages changed / navigation semantics changed; no new reachable surface added; do not update `docs/ui-ux-enterprise-audit/*` unless implementation materially changes navigation entries or routes (not expected)
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes
|
||||
- **Systems touched** (expected):
|
||||
- `apps/platform/app/Support/Navigation/WorkspaceHubNavigation.php`
|
||||
- `apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php`
|
||||
- `apps/platform/app/Filament/Concerns/UsesAdminEnvironmentFilterQueryParameter.php`
|
||||
- `apps/platform/app/Http/Middleware/EnsureWorkspaceSelected.php`
|
||||
- `apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php`
|
||||
- `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||
- `apps/platform/tests/Feature/Navigation/*`
|
||||
- **Shared abstractions reused**: existing hub filter contract helpers, `WorkspaceContext`, deny-as-not-found semantics, existing guard-test patterns
|
||||
- **New abstraction introduced?**: none
|
||||
- **Spread control**: inventory-driven; limit changes to URL/query scope semantics and canonical link generation only
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
N/A — no `OperationRun` start/completion/link UX changes.
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
### Phase 1 — Repo truth inventory + failing tests first
|
||||
|
||||
- inventory every remaining legacy scope query parsing seam (query keys only, not DB naming)
|
||||
- add/adjust Feature tests that fail on today’s behavior and encode the intended contract
|
||||
|
||||
### Phase 2 — Remove legacy query scope hint parsing in shared seams
|
||||
|
||||
- remove or strictly ignore legacy scope hints in:
|
||||
- `EnsureWorkspaceSelected`
|
||||
- `EnsureEnvironmentContextSelected`
|
||||
- `OperateHubShell`
|
||||
- `EnvironmentRequiredPermissions` (query-hint handling only)
|
||||
- ensure out-of-scope requests preserve deny-as-not-found semantics
|
||||
|
||||
### Phase 3 — Align canonical link generation
|
||||
|
||||
- ensure link generation:
|
||||
- never emits legacy scope query keys
|
||||
- uses `environment_id` only for workspace hub narrowing
|
||||
- does not treat `environment_id` as a replacement for route-owned environment pages
|
||||
|
||||
### Phase 4 — Regression guards
|
||||
|
||||
- add/extend guard tests that block:
|
||||
- parsing legacy scope keys as authority
|
||||
- generating navigation URLs containing forbidden scope query keys
|
||||
|
||||
### Phase 5 — Validation + close-out
|
||||
|
||||
- run the narrowest Feature tests
|
||||
- run `pint` on dirty files and `git diff --check`
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification**: Feature
|
||||
- **Affected validation lanes**: fast-feedback (Feature)
|
||||
- **Narrowest proving command(s)**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Navigation --filter=Spec341`
|
||||
- **Fixture / helper cost risks**: none expected; reuse existing workspace/environment factories + navigation harness helpers
|
||||
- **Escalation path**: `document-in-feature` if a bounded exception is proven necessary; otherwise treat legacy alias preservation as a blocker
|
||||
|
||||
## Deployment / Ops Impact
|
||||
|
||||
No migrations, env vars, queues, scheduler, storage, or asset pipeline changes are planned.
|
||||
|
||||
## Constitution Check (subset)
|
||||
|
||||
- Proportionality: no new persistence/abstractions/taxonomy introduced
|
||||
- Compatibility posture: legacy alias preservation is out of scope
|
||||
- Scope & isolation: route-owned environment remains authoritative; hub narrowing is `environment_id` only; deny-as-not-found semantics preserved
|
||||
191
specs/341-canonical-link-query-cleanup/spec.md
Normal file
191
specs/341-canonical-link-query-cleanup/spec.md
Normal file
@ -0,0 +1,191 @@
|
||||
# Feature Specification: Spec 341 - Canonical Link / Query Cleanup
|
||||
|
||||
**Feature Branch**: `341-canonical-link-query-cleanup`
|
||||
**Created**: 2026-05-31
|
||||
**Status**: Draft
|
||||
**Input**: Candidate `canonical-link-query-cleanup` from `docs/product/spec-candidates.md` + repo inspection of remaining legacy scope query hints in middleware/shell after Specs 338–340 and Spec 339.
|
||||
|
||||
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: The workspace/environment scope contract is now repo-real (Specs 314–322, 338–339), but some remaining URL/query seams still interpret legacy scope query keys (`tenant`, `managed_environment_id`, etc.) as “environment hints”. That reintroduces hidden scope, weakens trust for credential-adjacent and environment-bound surfaces, and makes deep links less predictable.
|
||||
- **Today's failure**: Repo inspection shows remaining legacy query parsing in shared request-scoping seams (e.g. environment/tenant selection middleware and `OperateHubShell`) that can treat `?tenant=` or `?managed_environment_id=` as scope hints outside workspace hubs. This undermines the “explicit or route-owned” scope rule and increases the risk of accidental scope drift resurfacing.
|
||||
- **User-visible improvement**: Operators can share and reload admin links confidently: workspace hubs are workspace-wide by default and only narrow via explicit `environment_id`; environment-bound pages are route-owned and do not accept legacy query aliases as authority; “clear filter” links return to clean canonical URLs; and guard tests block reintroducing legacy query scope behavior.
|
||||
- **Smallest enterprise-capable version**: Inventory remaining legacy scope query parsing, remove it (or make it strictly ignore-only with safe fallback), align canonical link generation across hubs/shell/middleware seams, and add targeted tests to prevent regressions.
|
||||
- **Explicit non-goals**:
|
||||
- No new UI shell/sidebar/topbar redesign.
|
||||
- No new routes or panels.
|
||||
- No new persisted entities, enums, or abstraction frameworks.
|
||||
- No RBAC model or capability registry changes.
|
||||
- No provider/OAuth redesign (Spec 281 family).
|
||||
- No Graph integration changes.
|
||||
- **Permanent complexity imported**: A small, explicit inventory + a small set of guard/contract tests around canonical URL/query semantics. No new runtime taxonomy or UI framework.
|
||||
- **Why now**: Specs 338–340 and Spec 339 hardened scope/authority. The next risk is “scope drift through links and query parsing” reappearing as features continue. This cleanup locks the contract so future work cannot accidentally revive legacy hints.
|
||||
- **Why not local**: Fixing a single page’s link generator is insufficient because the drift sits in shared request-scoping seams. The smallest correct slice must cover the shared parsing/link seams and prove behavior with tests.
|
||||
- **Approval class**: Core Enterprise (scope/authorization safety hardening).
|
||||
- **Red flags triggered**: Cross-surface navigation semantics + shared middleware/shell seams. **Defense**: bounded to removing legacy query hint parsing and aligning canonical URL generation; no new frameworking/persistence; tests make the change reviewable.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 1 | Komplexität: 1 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 9/12**
|
||||
- **Decision**: approve.
|
||||
|
||||
## Summary
|
||||
|
||||
Canonicalize admin navigation and request scope semantics so that:
|
||||
|
||||
- Workspace hubs remain workspace-wide unless explicitly filtered by `environment_id`.
|
||||
- Environment-bound pages derive environment context only from route parameters (not legacy query aliases).
|
||||
- Legacy scope query keys do not grant authority, do not silently narrow results, and do not reintroduce “hidden environment context”.
|
||||
- Shared seams (`EnsureWorkspaceSelected`, `EnsureEnvironmentContextSelected`, `OperateHubShell`) do not parse legacy query scope hints for authority.
|
||||
|
||||
This is a hardening / cleanup slice: it removes legacy scope query hint parsing and aligns link/query semantics with the existing scope contract.
|
||||
|
||||
## Completed-Spec Guardrail Result
|
||||
|
||||
Related specs are context only and must not be rewritten:
|
||||
|
||||
- Workspace hub contracts and legacy alias guards: Specs 314–322, 317, 338.
|
||||
- Credential-adjacent authority hardening: Spec 339.
|
||||
- Post-scope browser verification gate: Spec 340.
|
||||
|
||||
No `specs/341-*` package existed before this prep package was created.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: canonical-view (navigation + link/query contract hygiene).
|
||||
- **Primary routes / surfaces affected** (contract-level; exact list is inventory-driven):
|
||||
- Workspace hubs that accept environment narrowing via `environment_id` (e.g. Governance Inbox, Decision Register, Review Register, Evidence Overview, Audit Log, Alerts, Provider Connections index, Customer Review Workspace).
|
||||
- Environment-bound pages under `/admin/workspaces/{workspace}/environments/{environment}/...` that must not accept legacy query scope hints.
|
||||
- Shared request-scoping seams that currently inspect legacy query keys:
|
||||
- `apps/platform/app/Http/Middleware/EnsureWorkspaceSelected.php`
|
||||
- `apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php`
|
||||
- `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||
- `apps/platform/app/Filament/Pages/EnvironmentRequiredPermissions.php` (query-hint cleanup only)
|
||||
- **Data Ownership**: No schema changes. This slice changes URL/query handling and link generation only.
|
||||
- **RBAC**:
|
||||
- No new capabilities.
|
||||
- Existing 404 vs 403 semantics remain authoritative.
|
||||
- `environment_id` is a filter hint only; it must be validated against current workspace context + actor entitlement.
|
||||
|
||||
For canonical-view surfaces:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: Workspace hub clean URLs are workspace-wide and must not inherit remembered environment context, Filament tenant fallback, session table filters, or legacy query keys. If narrowed, the page must show an explicit `environment_id` filter state.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Any requested `environment_id` must resolve to an environment in the current workspace that the actor can access; foreign/out-of-scope IDs must fail as not found and must not leak existence.
|
||||
|
||||
## UI Surface Impact *(mandatory — UI-COV-001)*
|
||||
|
||||
Does this spec add, remove, rename, or materially change any reachable UI surface?
|
||||
|
||||
- [ ] No UI surface impact
|
||||
- [x] Existing page changed
|
||||
- [ ] New page/route added
|
||||
- [x] Navigation changed
|
||||
- [ ] Filament panel/provider surface changed
|
||||
- [ ] New modal/drawer/wizard/action added
|
||||
- [ ] New table/form/state added
|
||||
- [ ] Customer-facing surface changed
|
||||
- [ ] Dangerous action changed
|
||||
- [ ] Status/evidence/review presentation changed
|
||||
- [x] Workspace/environment context presentation changed
|
||||
|
||||
## UI/Productization Coverage *(UI-COV-001)*
|
||||
|
||||
- **Route/page/surface**: Cross-surface link + query semantics for workspace hubs and environment-bound pages (no new UI surface).
|
||||
- **Current page archetype**: scope-contract / navigation semantics (global-context-shell + workspace hub filter contract).
|
||||
- **Design depth**: Domain Pattern Surface (scope/authority hardening) — minimal visible UX change expected.
|
||||
- **Repo-truth level**: repo-verified (existing hub filter contracts + shared middleware/shell code).
|
||||
- **Existing pattern reused**: Spec 314/315/316 hub contracts + Spec 317 legacy alias cleanup + Spec 338 contract tests + existing navigation helpers.
|
||||
- **New pattern required**: none (tighten and converge; do not invent a new link-normalization framework).
|
||||
- **Screenshot required**: no (unless implementation introduces visible copy changes; then add one targeted screenshot in the implementation PR only).
|
||||
- **Page audit required**: no (no new archetype; contract hardening only).
|
||||
- **Customer-safe review required**: no (scope contract applies broadly; not a customer-safe productization slice).
|
||||
- **Dangerous-action review required**: no (no action behavior change; link/query semantics only).
|
||||
- **Coverage files to update (in implementation PR)**:
|
||||
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md` (only if routes/nav entries change materially; not expected)
|
||||
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` (no new surface; expected `no`)
|
||||
- [x] `N/A - no new reachable UI surface added; contract/URL semantics only`
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
|
||||
|
||||
- **Cross-cutting feature?**: yes.
|
||||
- **Interaction class(es)**: navigation deep links, scope presentation, URL/query semantics, “clear filter” behavior.
|
||||
- **Systems touched (expected)**:
|
||||
- `apps/platform/app/Support/Navigation/WorkspaceHubNavigation.php`
|
||||
- `apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php`
|
||||
- `apps/platform/app/Filament/Concerns/UsesAdminEnvironmentFilterQueryParameter.php`
|
||||
- `apps/platform/app/Http/Middleware/EnsureWorkspaceSelected.php`
|
||||
- `apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php`
|
||||
- `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||
- `apps/platform/tests/Feature/Navigation/*`
|
||||
- **Existing pattern(s) to extend**: Explicit `environment_id` filter only for workspace hubs; route-owned environment for environment-bound pages; deny-as-not-found for out-of-scope.
|
||||
- **Allowed deviation and why**: none. This slice removes legacy behavior; it does not add a second link/query interpretation path.
|
||||
- **Consistency impact**: One canonical environment filter key (`environment_id`) for hubs; no legacy query alias authority anywhere; all “clear filter” links return to clean canonical URLs.
|
||||
- **Review focus**: “legacy query scope hints are not authority” + tests that make regressions obvious.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
N/A — no `OperationRun` start, link, or lifecycle semantics are changed in this slice.
|
||||
|
||||
## Provider Boundary / Platform Core Check
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no.
|
||||
- **N/A**: This is platform scope/navigation hygiene. It must not interpret provider “tenant” identifiers as platform authority.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
N/A — no new persisted entities, abstraction frameworks, enums/status families, or taxonomy systems are introduced.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Feature (scope/link/query contract + regression guard).
|
||||
- **Validation lane(s)**: fast-feedback (Feature). Browser smoke is optional and only justified if implementation changes visible navigation/URL behavior that cannot be proven reliably in Feature tests alone.
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Navigation --filter=Canonical`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces --filter=Scope`
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- **AC1**: Workspace hubs accept `environment_id` as the only environment-narrowing query key; legacy aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) never establish filter authority.
|
||||
- **AC2**: Environment-bound routes derive environment context only from route params and do not parse legacy query aliases as scope hints.
|
||||
- **AC3**: “Clear filter” returns to the clean canonical hub URL and clears any persisted hub filter state as per existing contracts.
|
||||
- **AC4**: Out-of-scope `environment_id` is rejected with deny-as-not-found semantics and does not leak environment existence.
|
||||
- **AC5**: Targeted Feature tests fail loudly on regression and pass after implementation; no new heavy-governance or browser family is introduced without an explicit spec decision.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 — Share a canonical workspace hub link (Priority: P1)
|
||||
|
||||
As a workspace operator, I can share a link to a workspace hub that is either workspace-wide (clean URL) or explicitly narrowed to one environment using `environment_id`, and the page shows scope/filter state correctly after reload/back/forward.
|
||||
|
||||
**Independent Test**: A Feature test renders each critical hub with clean URL and with `?environment_id=<id>` and asserts filter state, clear-filter behavior, and no legacy query keys.
|
||||
|
||||
### User Story 2 — Legacy query aliases never become authority (Priority: P1)
|
||||
|
||||
As a security reviewer, I can trust that legacy query keys cannot establish environment context or widen results on environment-bound pages or shared scoping middleware.
|
||||
|
||||
**Independent Test**: Feature tests verify that `?tenant=` and `?managed_environment_id=` are ignored or rejected consistently and never treated as an environment hint for authority.
|
||||
|
||||
### User Story 3 — Guard tests prevent reintroduction (Priority: P2)
|
||||
|
||||
As a developer, I have regression guards that fail CI if new code introduces legacy query parsing or generates links containing legacy scope query keys.
|
||||
|
||||
**Independent Test**: A guard test scans/validates generated navigation URLs for forbidden query keys.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Any feature work that changes business logic, capabilities, policies, or data model.
|
||||
- Any broad UI redesign, new surfaces, or new navigation architecture.
|
||||
- Any provider/OAuth or credential workflow changes.
|
||||
- Any compatibility redirects or preserving legacy scope query keys “just in case”.
|
||||
|
||||
## Risks
|
||||
|
||||
- Some internal/shared surfaces may still rely on legacy query hints for environment selection. Implementation must replace that behavior with explicit route-owned context or explicit `environment_id` on hubs, and prove it with tests.
|
||||
- Over-scoping this cleanup can become a refactor. Tasks must stay inventory-driven and bounded to canonical scope/query semantics only.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The workspace/environment scope contract tests and hub filter contracts remain authoritative.
|
||||
- This is pre-production: removing legacy scope query keys is allowed and preferred over compatibility layers.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None blocking. Any discovered “needed but unclear” legacy behavior must be recorded as a follow-up candidate rather than silently preserved.
|
||||
93
specs/341-canonical-link-query-cleanup/tasks.md
Normal file
93
specs/341-canonical-link-query-cleanup/tasks.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Tasks: Spec 341 - Canonical Link / Query Cleanup
|
||||
|
||||
- Input: `specs/341-canonical-link-query-cleanup/spec.md`, `specs/341-canonical-link-query-cleanup/plan.md`
|
||||
- Preparation status: implementation-ready.
|
||||
|
||||
**Tests**: Required. This spec hardens scope/URL semantics and must be guarded by deterministic Feature tests.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is explicit and narrow: Feature (navigation/scope contract).
|
||||
- [x] No new default-heavy helpers/factories/seeds are introduced.
|
||||
- [x] Contract tests are written before refactors to keep review safe.
|
||||
- [x] Any exception resolves as `document-in-feature`, `follow-up-spec`, or `reject-or-split`.
|
||||
|
||||
## Phase 1: Preparation And Repo Truth (blocks runtime changes)
|
||||
|
||||
**Purpose**: Identify every remaining legacy scope query parsing seam and the canonical link helpers to reuse.
|
||||
|
||||
- [x] T001 Re-read `specs/341-canonical-link-query-cleanup/spec.md`, `specs/341-canonical-link-query-cleanup/plan.md`, and this `tasks.md`.
|
||||
- [x] T002 Confirm branch and working tree intent and record baseline commit (`git status --short --branch`, `git log -1 --oneline`).
|
||||
- [x] T003 Inventory legacy scope query parsing in runtime code (focus: request query keys, not DB column names):
|
||||
- `apps/platform/app/Http/Middleware/EnsureWorkspaceSelected.php` (`query('tenant')`, `query('managed_environment_id')`)
|
||||
- `apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php` (legacy query hints)
|
||||
- `apps/platform/app/Support/OperateHub/OperateHubShell.php` (legacy query tenant hints)
|
||||
- `apps/platform/app/Filament/Pages/EnvironmentRequiredPermissions.php` (legacy query hint)
|
||||
- Search guard: `rg -n \"query\\('tenant'\\)|query\\('managed_environment_id'\\)\" apps/platform/app`
|
||||
- [x] T004 Inventory existing navigation contract tests and decide where Spec 341 regression coverage belongs:
|
||||
- `apps/platform/tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php`
|
||||
- `apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php`
|
||||
- `apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php`
|
||||
- `apps/platform/tests/Feature/Workspaces/WorkspaceHubContextContractTest.php`
|
||||
|
||||
## Phase 2: Add failing contract tests first
|
||||
|
||||
**Purpose**: Make the cleanup reviewable and prevent accidental reintroduction of legacy scope hints.
|
||||
|
||||
- [x] T005 Add a Spec 341 Feature test proving legacy scope query keys do not establish authority in shared seams:
|
||||
- Requests with `?tenant=` or `?managed_environment_id=` do not establish environment context and do not widen access.
|
||||
- Implement in: `apps/platform/tests/Feature/Navigation/Spec341CanonicalLinkQueryCleanupTest.php`
|
||||
- [x] T006 [P] Add a test proving workspace hub narrowing is `environment_id`-only:
|
||||
- Every in-scope hub URL accepts `environment_id`;
|
||||
- legacy aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) are rejected/ignored.
|
||||
- Extend/verify: `apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php`
|
||||
- [x] T007 [P] Add a test proving environment-bound pages remain route-owned:
|
||||
- No environment-bound route derives environment scope from legacy query keys.
|
||||
- Target at least one representative environment-bound route (e.g. Baseline Compare) plus one middleware-driven entry.
|
||||
|
||||
## Phase 3: Remove legacy query scope hint parsing (Spec 341 contract)
|
||||
|
||||
**Purpose**: Remove hidden environment authority sources and converge on explicit `environment_id` (hubs) or route-owned environment context (environment-bound pages).
|
||||
|
||||
- [x] T008 Update `apps/platform/app/Http/Middleware/EnsureWorkspaceSelected.php`:
|
||||
- remove `?tenant=` / `?managed_environment_id=` handling as “explicit tenant context” signals;
|
||||
- keep workspace selection and deny-as-not-found semantics correct.
|
||||
- [x] T009 Update `apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php`:
|
||||
- remove legacy scope query hint parsing;
|
||||
- ensure workspace hubs remain exempt from tenant/environment selection requirements.
|
||||
- [x] T010 Update `apps/platform/app/Support/OperateHub/OperateHubShell.php`:
|
||||
- remove `resolveQueryTenantHint()` and related “explicit query tenant hint” behavior (or convert to ignore-only with no authority);
|
||||
- ensure all environment-bound context comes from route parameters + validated workspace context.
|
||||
- [x] T011 Update `apps/platform/app/Filament/Pages/EnvironmentRequiredPermissions.php`:
|
||||
- remove legacy query hint parsing; route-owned environment only.
|
||||
|
||||
## Phase 4: Align canonical link generation
|
||||
|
||||
**Purpose**: Ensure the code never generates legacy scope query keys and that “clear filter” links return to clean canonical URLs.
|
||||
|
||||
- [x] T012 Remove/replace any link generation that emits legacy scope query keys (focus: URL query keys, not Graph tenant context):
|
||||
- Use `rg -n \"\\?tenant=|\\btenant=\\\"|query\\('tenant'\\)\" apps/platform` to find offenders.
|
||||
- [x] T013 Confirm `apps/platform/app/Support/Navigation/WorkspaceHubNavigation.php` and `WorkspaceHubEnvironmentFilter.php` remain the single source for hub filter link building and parsing (`environment_id` only).
|
||||
- [x] T014 Confirm `apps/platform/app/Filament/Concerns/UsesAdminEnvironmentFilterQueryParameter.php` continues to strip legacy scope keys and that no new legacy keys are added elsewhere.
|
||||
|
||||
## Phase 5: Regression guards
|
||||
|
||||
**Purpose**: Prevent future drift back to legacy scope query keys.
|
||||
|
||||
- [x] T015 Add a guard test that fails if generated navigation URLs contain forbidden scope query keys (e.g. `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`).
|
||||
- [x] T016 If a bounded exception is proven necessary, document it explicitly in the spec/PR and add a dedicated test; otherwise treat legacy scope alias preservation as a blocker.
|
||||
|
||||
## Phase 6: Validation
|
||||
|
||||
- [x] T017 Run narrow tests first:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Navigation --filter=Spec341`
|
||||
- [x] T018 Run formatting and patch checks:
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
- `git diff --check`
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- [x] NT001 Do not add migrations, new tables, or new persisted truth.
|
||||
- [x] NT002 Do not introduce a new link-normalization abstraction framework.
|
||||
- [x] NT003 Do not add compatibility redirects for legacy query keys.
|
||||
- [x] NT004 Do not change provider/OAuth behavior or credential flows (Spec 281 family).
|
||||
Loading…
Reference in New Issue
Block a user