feat: enforce environment-owned baseline compare routing (#374)

## Summary
- move Baseline Compare onto the canonical workspace plus environment owned route instead of workspace-style access
- remove legacy environment query and remembered-context fallback paths from the affected Baseline Compare entry points and shell handling
- update related navigation, support links, and regression coverage for admin surface scope and managed environment route contracts
- add Spec 319 artifacts for the environment-owned surface routing and shell context contract

## Testing
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php tests/Feature/Filament/BaselineCompareLandingDuplicateNamesBannerTest.php tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php tests/Feature/Filament/PanelNavigationSegregationTest.php tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php tests/Feature/Navigation/WorkspaceHubRegistryTest.php tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php tests/Feature/Rbac/DriftLandingUiEnforcementTest.php tests/Unit/Tenants/AdminSurfaceScopeTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #374
This commit is contained in:
ahmido 2026-05-16 20:45:39 +00:00
parent 1c27af4f5f
commit ddf7c15c52
23 changed files with 493 additions and 98 deletions

View File

@ -4,14 +4,13 @@
namespace App\Filament\Pages;
use App\Filament\Concerns\ResolvesPanelTenantContext;
use App\Filament\Concerns\UsesAdminEnvironmentFilterQueryParameter;
use App\Filament\Resources\BaselineProfileResource;
use App\Filament\Resources\FindingResource;
use App\Models\BaselineProfile;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Services\Baselines\BaselineCompareService;
use App\Support\Auth\Capabilities;
@ -31,17 +30,18 @@
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Model;
use Livewire\Attributes\Locked;
use UnitEnum;
class BaselineCompareLanding extends Page
{
use ResolvesPanelTenantContext;
use UsesAdminEnvironmentFilterQueryParameter;
protected const MONITORING_PAGE_STATE_CONTRACT = [
'surfaceKey' => 'baseline_compare_landing',
'surfaceType' => 'launch_context_support',
@ -96,6 +96,16 @@ class BaselineCompareLanding extends Page
'localOnlyStateKeys' => [],
];
private const LEGACY_SCOPE_QUERY_KEYS = [
'environment_id',
'tenant',
'tenant_id',
'managed_environment_id',
'environment',
'tenant_scope',
'tableFilters',
];
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-scale';
protected static string|UnitEnum|null $navigationGroup = 'Governance';
@ -106,6 +116,8 @@ class BaselineCompareLanding extends Page
protected static ?string $title = 'Baseline Compare';
protected static ?string $slug = 'workspaces/{workspace}/environments/{environment}/baseline-compare';
protected string $view = 'filament.pages.baseline-compare-landing';
public static function shouldRegisterNavigation(): bool
@ -184,23 +196,44 @@ public static function shouldRegisterNavigation(): bool
public ?string $matrixSubjectKey = null;
#[Locked]
public ?int $scopedEnvironmentId = null;
/**
* @param array<mixed> $parameters
*/
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
{
$panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin';
if ($panelId !== 'admin') {
return parent::getUrl($parameters, $isAbsolute, $panelId, $tenant);
}
$environment = static::resolveAdminUrlEnvironment($parameters, $tenant);
if (! $environment instanceof ManagedEnvironment) {
return url('/admin');
}
$workspace = static::resolveAdminUrlWorkspace($environment, $parameters);
if (! $workspace instanceof Workspace && ! is_string($workspace) && ! is_int($workspace)) {
return url('/admin');
}
$parameters = static::withoutLegacyScopeQuery($parameters);
$parameters['environment'] = $environment;
$parameters['workspace'] = $workspace instanceof Workspace
? static::workspaceRouteKey($workspace)
: $workspace;
return parent::getUrl($parameters, $isAbsolute, $panelId, null);
}
public static function canAccess(): bool
{
$user = auth()->user();
if (! $user instanceof User) {
return false;
}
$tenant = static::resolveTenantContextForCurrentPanel();
if (! $tenant instanceof ManagedEnvironment) {
return false;
}
$resolver = app(CapabilityResolver::class);
return $resolver->can($user, $tenant, Capabilities::TENANT_VIEW);
return static::hasEnvironmentAccess(static::resolveRouteOwnedEnvironment());
}
/**
@ -211,20 +244,31 @@ public static function monitoringPageStateContract(): array
return self::MONITORING_PAGE_STATE_CONTRACT;
}
public function mount(): void
public function mount(ManagedEnvironment|string|null $environment = null): void
{
$tenant = static::resolveRouteOwnedEnvironment($environment);
if (! $tenant instanceof ManagedEnvironment || ! static::hasEnvironmentAccess($tenant)) {
abort(404);
}
$this->scopedEnvironmentId = (int) $tenant->getKey();
$this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null;
$baselineProfileId = request()->query('baseline_profile_id');
$subjectKey = request()->query('subject_key');
$this->matrixBaselineProfileId = is_numeric($baselineProfileId) ? (int) $baselineProfileId : null;
$this->matrixSubjectKey = is_string($subjectKey) && trim($subjectKey) !== '' ? trim($subjectKey) : null;
$this->refreshStats();
$this->refreshStatsForEnvironment($tenant);
}
public function refreshStats(): void
{
$tenant = static::resolveTenantContextForCurrentPanel();
$this->refreshStatsForEnvironment($this->currentEnvironment());
}
private function refreshStatsForEnvironment(?ManagedEnvironment $tenant): void
{
$stats = BaselineCompareStats::forTenant($tenant);
$aggregate = $tenant instanceof ManagedEnvironment
? $this->governanceAggregate($tenant, $stats)
@ -449,10 +493,10 @@ private function compareNowAction(): Action
return;
}
$tenant = static::resolveTenantContextForCurrentPanel();
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
Notification::make()->title('Select a tenant to compare baselines')->danger()->send();
Notification::make()->title('Open an environment to compare baselines')->danger()->send();
return;
}
@ -516,7 +560,7 @@ private function compareNowAction(): Action
public function getFindingsUrl(): ?string
{
$tenant = static::resolveTenantContextForCurrentPanel();
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return null;
@ -531,7 +575,7 @@ public function getRunUrl(): ?string
return null;
}
$tenant = static::resolveTenantContextForCurrentPanel();
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return null;
@ -582,7 +626,7 @@ private function navigationContext(): ?CanonicalNavigationContext
private function resolveCompareMatrixProfile(): ?BaselineProfile
{
$tenant = static::resolveTenantContextForCurrentPanel();
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return null;
@ -606,4 +650,203 @@ private function resolveCompareMatrixProfile(): ?BaselineProfile
return null;
}
private function currentEnvironment(): ?ManagedEnvironment
{
if ($this->scopedEnvironmentId !== null) {
$tenant = ManagedEnvironment::query()->whereKey($this->scopedEnvironmentId)->first();
return $tenant instanceof ManagedEnvironment && static::hasEnvironmentAccess($tenant)
? $tenant
: null;
}
$tenant = static::resolveRouteOwnedEnvironment();
return $tenant instanceof ManagedEnvironment && static::hasEnvironmentAccess($tenant)
? $tenant
: null;
}
protected static function resolveRouteOwnedEnvironment(ManagedEnvironment|string|null $environment = null): ?ManagedEnvironment
{
if ($environment instanceof ManagedEnvironment) {
return $environment;
}
if (is_string($environment) && $environment !== '') {
return ManagedEnvironment::query()
->where('slug', $environment)
->first();
}
$routeEnvironment = request()->route('environment');
if ($routeEnvironment instanceof ManagedEnvironment) {
return $routeEnvironment;
}
if (is_string($routeEnvironment) && $routeEnvironment !== '') {
return ManagedEnvironment::query()
->where('slug', $routeEnvironment)
->first();
}
return static::resolveRefererOwnedEnvironment();
}
private static function hasEnvironmentAccess(?ManagedEnvironment $environment): bool
{
$user = auth()->user();
if (! $environment instanceof ManagedEnvironment || ! $user instanceof User) {
return false;
}
$routeWorkspace = request()->route('workspace');
if ($routeWorkspace instanceof Workspace && (int) $routeWorkspace->getKey() !== (int) $environment->workspace_id) {
return false;
}
if (is_string($routeWorkspace) && $routeWorkspace !== '') {
$workspace = $environment->workspace instanceof Workspace
? $environment->workspace
: $environment->workspace()->first();
if (! $workspace instanceof Workspace) {
return false;
}
if ($routeWorkspace !== static::workspaceRouteKey($workspace) && $routeWorkspace !== (string) $workspace->getKey()) {
return false;
}
}
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if ($workspaceId !== null && (int) $workspaceId !== (int) $environment->workspace_id) {
return false;
}
/** @var CapabilityResolver $resolver */
$resolver = app(CapabilityResolver::class);
return $resolver->isMember($user, $environment)
&& $resolver->can($user, $environment, Capabilities::TENANT_VIEW);
}
/**
* @param array<mixed> $parameters
*/
private static function resolveAdminUrlEnvironment(array $parameters, ?Model $tenant = null): ?ManagedEnvironment
{
$parameterEnvironment = $parameters['tenant'] ?? $parameters['environment'] ?? null;
if ($parameterEnvironment instanceof ManagedEnvironment) {
return $parameterEnvironment;
}
if ($tenant instanceof ManagedEnvironment) {
return $tenant;
}
$routeEnvironment = static::resolveRouteOwnedEnvironment();
if ($routeEnvironment instanceof ManagedEnvironment) {
return $routeEnvironment;
}
$filamentTenant = Filament::getTenant();
return $filamentTenant instanceof ManagedEnvironment ? $filamentTenant : null;
}
/**
* @param array<mixed> $parameters
*/
private static function resolveAdminUrlWorkspace(ManagedEnvironment $environment, array $parameters): Workspace|string|int|null
{
$workspace = $parameters['workspace'] ?? null;
if ($workspace instanceof Workspace || is_string($workspace) || is_int($workspace)) {
return $workspace;
}
$environmentWorkspace = $environment->workspace;
if ($environmentWorkspace instanceof Workspace) {
return $environmentWorkspace;
}
return $environment->workspace()->first();
}
/**
* @param array<mixed> $parameters
* @return array<mixed>
*/
private static function withoutLegacyScopeQuery(array $parameters): array
{
foreach (self::LEGACY_SCOPE_QUERY_KEYS as $key) {
unset($parameters[$key]);
}
return $parameters;
}
private static function workspaceRouteKey(Workspace $workspace): string
{
$slug = $workspace->getAttribute('slug');
return is_string($slug) && $slug !== ''
? $slug
: (string) $workspace->getKey();
}
private static function resolveRefererOwnedEnvironment(): ?ManagedEnvironment
{
$referer = request()->headers->get('referer');
if (! is_string($referer) || $referer === '') {
return null;
}
$path = parse_url($referer, PHP_URL_PATH);
if (! is_string($path)) {
return null;
}
if (preg_match('#^/admin/workspaces/([^/]+)/environments/([^/]+)/baseline-compare$#', $path, $matches) !== 1) {
return null;
}
$workspaceRouteKey = rawurldecode($matches[1]);
$environmentRouteKey = rawurldecode($matches[2]);
$environment = ManagedEnvironment::query()
->where('slug', $environmentRouteKey)
->first();
if (! $environment instanceof ManagedEnvironment) {
return null;
}
$workspace = $environment->workspace;
if (! $workspace instanceof Workspace) {
$workspace = $environment->workspace()->first();
}
if (! $workspace instanceof Workspace) {
return null;
}
if ($workspaceRouteKey !== static::workspaceRouteKey($workspace) && $workspaceRouteKey !== (string) $workspace->getKey()) {
return null;
}
return $environment;
}
}

View File

@ -16,6 +16,7 @@
use App\Support\Baselines\BaselineCompareMatrixBuilder;
use App\Support\Baselines\BaselineReasonCodes;
use App\Support\Baselines\Compare\CompareStrategyRegistry;
use App\Support\ManagedEnvironmentLinks;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperationRunLinks;
use App\Support\OperationRunType;
@ -445,10 +446,7 @@ public function tenantCompareUrl(int $tenantId, ?string $subjectKey = null): ?st
return null;
}
return BaselineCompareLanding::getUrl(
parameters: $this->navigationContext($tenant, $subjectKey)->toQuery(),
tenant: $tenant,
);
return ManagedEnvironmentLinks::baselineCompareUrl($tenant, $this->navigationContext($tenant, $subjectKey)->toQuery());
}
public function findingUrl(int $tenantId, int $findingId, ?string $subjectKey = null): ?string

View File

@ -4,16 +4,16 @@
namespace App\Filament\Widgets\Dashboard;
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Resources\FindingResource;
use App\Models\FindingException;
use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\User;
use App\Support\Auth\Capabilities;
use App\Support\Baselines\BaselineCompareSummaryAssessment;
use App\Support\Baselines\TenantGovernanceAggregate;
use App\Support\Baselines\TenantGovernanceAggregateResolver;
use App\Support\ManagedEnvironmentLinks;
use App\Support\OperationRunLinks;
use App\Support\Rbac\UiTooltips;
use Filament\Facades\Filament;
@ -53,7 +53,7 @@ protected function getViewData(): array
return $empty;
}
$tenantLandingUrl = BaselineCompareLanding::getUrl(tenant: $tenant);
$tenantLandingUrl = ManagedEnvironmentLinks::baselineCompareUrl($tenant);
$operationsFollowUpCount = (int) OperationRun::query()
->where('managed_environment_id', (int) $tenant->getKey())
->dashboardNeedsFollowUp()

View File

@ -4,15 +4,14 @@
namespace App\Filament\Widgets\Dashboard;
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Resources\BackupScheduleResource;
use App\Filament\Resources\BackupSetResource;
use App\Filament\Resources\FindingResource;
use App\Filament\Resources\RestoreRunResource;
use App\Models\FindingException;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Support\Auth\Capabilities;
use App\Support\BackupHealth\BackupHealthActionTarget;
@ -21,6 +20,7 @@
use App\Support\BackupHealth\TenantBackupHealthResolver;
use App\Support\Baselines\TenantGovernanceAggregate;
use App\Support\Baselines\TenantGovernanceAggregateResolver;
use App\Support\ManagedEnvironmentLinks;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\ActiveRuns;
use App\Support\Rbac\UiTooltips;
@ -164,7 +164,7 @@ protected function getViewData(): array
'badge' => 'Baseline',
'badgeColor' => $compareAssessment->tone,
'actionLabel' => 'Open Baseline Compare',
'actionUrl' => BaselineCompareLanding::getUrl(tenant: $tenant),
'actionUrl' => ManagedEnvironmentLinks::baselineCompareUrl($tenant),
];
}

View File

@ -4,10 +4,10 @@
namespace App\Filament\Widgets\ManagedEnvironment;
use App\Filament\Pages\BaselineCompareLanding;
use App\Models\ManagedEnvironment;
use App\Support\Baselines\TenantGovernanceAggregate;
use App\Support\Baselines\TenantGovernanceAggregateResolver;
use App\Support\ManagedEnvironmentLinks;
use App\Support\OperationRunLinks;
use Filament\Facades\Filament;
use Filament\Widgets\Widget;
@ -36,7 +36,7 @@ protected function getViewData(): array
? OperationRunLinks::view($aggregate->stats->operationRunId, $tenant)
: null;
$landingUrl = BaselineCompareLanding::getUrl(tenant: $tenant);
$landingUrl = ManagedEnvironmentLinks::baselineCompareUrl($tenant);
$nextActionUrl = match ($aggregate->nextActionTarget) {
'run' => $runUrl,
'findings' => \App\Filament\Resources\FindingResource::getUrl('index', tenant: $tenant),

View File

@ -4,7 +4,6 @@
namespace App\Support\EnvironmentDashboard;
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Resources\BackupScheduleResource;
use App\Filament\Resources\BackupSetResource;
@ -32,6 +31,7 @@
use App\Support\Baselines\TenantGovernanceAggregate;
use App\Support\Baselines\TenantGovernanceAggregateResolver;
use App\Support\Links\RequiredPermissionsLinks;
use App\Support\ManagedEnvironmentLinks;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
@ -1443,7 +1443,7 @@ private function baselineCompareAction(ManagedEnvironment $tenant, ?User $user,
return $this->actionPayload(
label: $label,
url: $canOpen ? BaselineCompareLanding::getUrl(tenant: $tenant) : null,
url: $canOpen ? ManagedEnvironmentLinks::baselineCompareUrl($tenant) : null,
helperText: $canOpen ? null : $this->overviewText('helper_baseline_compare_requires_permissions'),
);
}

View File

@ -2,6 +2,7 @@
namespace App\Support;
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Resources\ProviderConnectionResource;
use App\Models\ManagedEnvironment;
use App\Models\ProviderConnection;
@ -68,6 +69,14 @@ public static function accessScopesUrl(ManagedEnvironment $environment, array $q
return self::environmentChildUrl('admin.workspace.environments.access-scopes', $environment, $query);
}
/**
* @param array<string, mixed> $query
*/
public static function baselineCompareUrl(ManagedEnvironment $environment, array $query = []): string
{
return BaselineCompareLanding::getUrl($query, panel: 'admin', tenant: $environment);
}
/**
* @param array<string, mixed> $query
*/

View File

@ -165,7 +165,7 @@ private function configureNavigationForRequest(\Filament\Panel $panel, Request $
private function isWorkspaceScopedPageWithTenant(string $path): bool
{
return preg_match('#^/admin/workspaces/[^/]+/environments/[^/]+/(required-permissions|diagnostics|access-scopes)$#', $path) === 1;
return preg_match('#^/admin/workspaces/[^/]+/environments/[^/]+/(required-permissions|diagnostics|access-scopes|baseline-compare)$#', $path) === 1;
}
private function isLivewireUpdatePath(string $path): bool

View File

@ -2,7 +2,6 @@
namespace App\Support;
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Resources\BackupScheduleResource;
use App\Filament\Resources\BackupSetResource;
use App\Filament\Resources\BaselineSnapshotResource;
@ -186,7 +185,7 @@ public static function related(OperationRun $run, ?ManagedEnvironment $tenant):
}
if ($canonicalType === 'baseline.compare') {
$links['Drift'] = BaselineCompareLanding::getUrl(tenant: $tenant);
$links['Drift'] = ManagedEnvironmentLinks::baselineCompareUrl($tenant);
}
if ($canonicalType === 'baseline.capture') {

View File

@ -4,7 +4,6 @@
namespace App\Support\Workspaces;
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Pages\ChooseEnvironment;
use App\Filament\Pages\Findings\FindingsHygieneReport;
use App\Filament\Pages\Findings\MyFindingsInbox;
@ -12,16 +11,16 @@
use App\Models\AlertDelivery;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentTriageReview;
use App\Models\OperationRun;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\Findings\FindingAssignmentHygieneService;
use App\Services\Auth\WorkspaceRoleCapabilityMap;
use App\Services\Findings\FindingAssignmentHygieneService;
use App\Support\Auth\Capabilities;
use App\Support\BackupHealth\TenantBackupHealthAssessment;
use App\Support\BackupHealth\TenantBackupHealthResolver;
@ -30,13 +29,13 @@
use App\Support\Baselines\BaselineCompareSummaryAssessment;
use App\Support\Baselines\TenantGovernanceAggregate;
use App\Support\Baselines\TenantGovernanceAggregateResolver;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\ManagedEnvironmentLinks;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
use App\Support\PortfolioTriage\ManagedEnvironmentTriageReviewStateResolver;
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
use App\Support\Rbac\UiTooltips;
use App\Support\RestoreSafety\RestoreResultAttention;
use App\Support\RestoreSafety\RestoreSafetyCopy;
@ -1571,8 +1570,7 @@ private function tenantDashboardTarget(
User $user,
string $label = 'Open environment',
?array $arrivalState = null,
): array
{
): array {
if (! $this->canAccessEnvironmentDashboard($user, $tenant)) {
return $this->disabledDestination(
kind: 'tenant_dashboard',
@ -1683,7 +1681,7 @@ private function baselineCompareTarget(ManagedEnvironment $tenant, User $user, s
return $this->destination(
kind: 'baseline_compare_landing',
url: BaselineCompareLanding::getUrl(tenant: $tenant),
url: ManagedEnvironmentLinks::baselineCompareUrl($tenant),
label: $label,
tenant: $tenant,
);

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\BaselineCompareLanding;
use App\Models\ManagedEnvironment;
use App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder;
use App\Support\ManagedEnvironmentLinks;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Support\Arr;
function baselineCompareRouteContractForbiddenQueryKeys(): array
{
return [
'environment_id',
'tenant',
'tenant_id',
'managed_environment_id',
'environment',
'tenant_scope',
'tableFilters',
];
}
it('generates a canonical environment-owned baseline compare URL without legacy scope query keys', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
$url = ManagedEnvironmentLinks::baselineCompareUrl($tenant, [
'environment_id' => (int) $tenant->getKey(),
'tenant' => (string) $tenant->external_id,
'tenant_id' => (int) $tenant->getKey(),
'managed_environment_id' => (int) $tenant->getKey(),
'environment' => 'legacy-query-value',
'tenant_scope' => 'selected',
'tableFilters' => ['managed_environment_id' => ['value' => (int) $tenant->getKey()]],
'baseline_profile_id' => 42,
'subject_key' => 'wifi-corp-profile',
]);
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
expect((string) parse_url($url, PHP_URL_PATH))
->toBe(sprintf(
'/admin/workspaces/%s/environments/%s/baseline-compare',
$tenant->workspace()->firstOrFail()->slug,
$tenant->getRouteKey(),
))
->and(array_keys($query))->not->toContain(...baselineCompareRouteContractForbiddenQueryKeys())
->and($query)->toMatchArray([
'baseline_profile_id' => '42',
'subject_key' => 'wifi-corp-profile',
]);
});
it('renders baseline compare from the route-owned environment context', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(ManagedEnvironmentLinks::baselineCompareUrl($tenant))
->assertOk()
->assertSeeText('Baseline Compare')
->assertSeeText($tenant->workspace()->firstOrFail()->name)
->assertSeeText($tenant->name)
->assertSeeText('This environment has no baseline assignment');
});
it('rejects old workspace-style baseline compare URLs and remembered environment fallback', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
Filament::setTenant(null, true);
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
(string) $tenant->workspace_id => (int) $tenant->getKey(),
],
])
->get('/admin/baseline-compare-landing?environment_id='.(int) $tenant->getKey())
->assertNotFound();
expect(BaselineCompareLanding::canAccess())->toBeFalse();
});
it('rejects baseline compare when the workspace route and environment route disagree', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$foreignTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Foreign Environment']);
createUserWithTenant(tenant: $foreignTenant, user: $user, role: 'owner');
$url = sprintf(
'/admin/workspaces/%s/environments/%s/baseline-compare',
$tenant->workspace()->firstOrFail()->slug,
$foreignTenant->getRouteKey(),
);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get($url)
->assertNotFound();
});
it('emits the environment-owned route from the environment dashboard baseline compare action', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$summary = app(EnvironmentDashboardSummaryBuilder::class)
->build($tenant, $user)
->toArray();
$baselineCompareStatus = Arr::first(
$summary['governanceStatus'],
static fn (array $status): bool => ($status['key'] ?? null) === 'baseline_compare',
);
$url = is_array($baselineCompareStatus) ? ($baselineCompareStatus['actionUrl'] ?? null) : null;
parse_str((string) parse_url((string) $url, PHP_URL_QUERY), $query);
expect($url)->toBe(ManagedEnvironmentLinks::baselineCompareUrl($tenant))
->and((string) parse_url((string) $url, PHP_URL_PATH))->toEndWith('/environments/'.$tenant->getRouteKey().'/baseline-compare')
->and(array_keys($query))->not->toContain(...baselineCompareRouteContractForbiddenQueryKeys());
});

View File

@ -13,7 +13,6 @@
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\Feature\Baselines\Support\BaselineSubjectResolutionFixtures;
uses(RefreshDatabase::class);
@ -97,7 +96,7 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant
]);
}
it('can access baseline compare when only the remembered admin tenant is available', function (): void {
it('does not authorize baseline compare from only the remembered admin environment', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
@ -110,7 +109,10 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant
(string) $tenant->workspace_id => (int) $tenant->getKey(),
]);
expect(BaselineCompareLanding::canAccess())->toBeTrue();
expect(BaselineCompareLanding::canAccess())->toBeFalse();
$this->get('/admin/baseline-compare-landing')
->assertNotFound();
});
it('renders tenant landing evidence-gap details with the same search affordances as the canonical run detail', function (): void {
@ -122,7 +124,7 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant
seedBaselineCompareLandingGapRun($tenant);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSee('Evidence gap details')
->assertSee('Search gap details')
->assertSee('Search by reason, type, class, outcome, action, or subject key')
@ -147,7 +149,7 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant
seedBaselineCompareLandingGapRun($tenant);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSee('Evidence gap details')
->assertSee('WiFi-Corp-Profile')
->assertSee('Baseline compare evidence');

View File

@ -1,12 +1,10 @@
<?php
use App\Filament\Pages\BaselineCompareLanding;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment;
use App\Models\InventoryItem;
use Filament\Facades\Filament;
use Livewire\Livewire;
it('shows a warning banner when duplicate policy names make baseline matching ambiguous', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -59,7 +57,7 @@
'last_seen_at' => now(),
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSee(__('baseline-compare.duplicate_warning_title'))
->assertSee('share generic display names')
->assertSee('resulting in 1 ambiguous subject')
@ -139,7 +137,7 @@
'last_seen_at' => now(),
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertDontSee(__('baseline-compare.duplicate_warning_title'))
->assertDontSee('share generic display names')
->assertDontSee('cannot match them safely to the baseline');

View File

@ -1,6 +1,5 @@
<?php
use App\Filament\Pages\BaselineCompareLanding;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment;
@ -10,7 +9,6 @@
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use Filament\Facades\Filament;
use Livewire\Livewire;
it('shows RBAC-specific baseline compare labels and assignment exclusion messaging', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -104,7 +102,7 @@
],
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSee('RBAC role definitions')
->assertSee('Compared')
->assertSee('Modified')

View File

@ -21,7 +21,6 @@
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Tests\Feature\Baselines\Support\FakeCompareStrategy;
use Tests\Feature\Baselines\Support\FakeGovernanceSubjectTaxonomyRegistry;
@ -67,7 +66,7 @@
'baseline_profile_id' => (int) $profile->getKey(),
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertActionVisible('compareNow')
->assertActionDisabled('compareNow')
->assertDontSee('Monitoring landing')
@ -106,7 +105,7 @@
'baseline_profile_id' => (int) $profile->getKey(),
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertActionHasLabel('compareNow', 'Compare now (full content)')
->callAction('compareNow')
->assertDispatchedTo(BulkOperationProgress::class, OpsUxBrowserEvents::RunEnqueued, tenantId: (int) $tenant->getKey());
@ -148,7 +147,7 @@
'baseline_profile_id' => (int) $profile->getKey(),
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertActionExists('compareNow', function (Action $action): bool {
return $action->getLabel() === 'Compare now (full content)'
&& $action->isConfirmationRequired()
@ -184,7 +183,7 @@
'baseline_profile_id' => (int) $profile->getKey(),
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertActionHasLabel('compareNow', 'Compare now (full content)')
->assertActionEnabled('compareNow')
->callAction('compareNow')
@ -244,7 +243,7 @@
'baseline_profile_id' => (int) $profile->getKey(),
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->callAction('compareNow')
->assertNotified('Cannot start comparison')
->assertStatus(200);
@ -260,7 +259,7 @@
$tenant->makeCurrent();
Filament::setTenant($tenant, true);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->call('refreshStats')
->assertStatus(200);
});
@ -289,10 +288,10 @@
'baseline_profile_id' => (int) $profile->getKey(),
]);
$component = Livewire::withQueryParams([
$component = baselineCompareLandingLivewire($tenant, [
'baseline_profile_id' => (int) $profile->getKey(),
'subject_key' => 'wifi-corp-profile',
])->test(BaselineCompareLanding::class);
]);
expect($component->instance()->openCompareMatrixUrl())
->toBe(BaselineProfileResource::compareMatrixUrl($profile).'?subject_key=wifi-corp-profile');
@ -346,7 +345,7 @@
],
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->call('refreshStats')
->assertSet('operationRunId', (int) $compareRun->getKey())
->assertSet('coverageStatus', 'ok')
@ -400,7 +399,7 @@
],
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->call('refreshStats')
->assertSet('operationRunId', (int) $compareRun->getKey())
->assertSet('coverageStatus', 'warning')
@ -444,7 +443,7 @@
],
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSet('state', 'no_snapshot')
->assertSet('snapshotId', null)
->assertSet('message', 'The latest inventory sync failed, so this capture could not use a credible upstream basis.');
@ -492,7 +491,7 @@
],
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSet('state', 'idle')
->assertSet('snapshotId', (int) $snapshot->getKey())
->assertActionEnabled('compareNow');

View File

@ -1,6 +1,5 @@
<?php
use App\Filament\Pages\BaselineCompareLanding;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment;
@ -11,7 +10,6 @@
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use Filament\Facades\Filament;
use Livewire\Livewire;
it('shows a clear why-no-findings explanation when the last run had zero drift', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -62,7 +60,7 @@
$summary = BaselineCompareStats::forTenant($tenant)->summaryAssessment();
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSee($summary->headline)
->assertSee('Aligned');
});
@ -120,7 +118,7 @@
],
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSee('Evidence gap details')
->assertSee('Detailed rows were not recorded for this run')
->assertSee('Baseline compare evidence');
@ -178,7 +176,7 @@
],
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertDontSee('Evidence gap details')
->assertSee('Baseline compare evidence');
});
@ -248,7 +246,7 @@
],
]);
Livewire::test(BaselineCompareLanding::class)
baselineCompareLandingLivewire($tenant)
->assertSee('Baseline compare evidence')
->assertSee('intune_policy')
->assertSee('strategy_failed')

View File

@ -363,6 +363,8 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a
$response->assertSeeText('Baselines');
$response->assertSeeText('Baseline Snapshots');
$response->assertSeeText('Baseline Compare');
$response->assertSee((string) parse_url(ManagedEnvironmentLinks::baselineCompareUrl($tenant), PHP_URL_PATH), false);
$response->assertDontSee('/admin/baseline-compare-landing', false);
$response->assertSeeText('Evidence');
$response->assertSeeText('Risk exceptions');

View File

@ -20,6 +20,7 @@
ManagedEnvironmentLinks::requiredPermissionsUrl($tenant),
ManagedEnvironmentLinks::diagnosticsUrl($tenant),
ManagedEnvironmentLinks::accessScopesUrl($tenant),
ManagedEnvironmentLinks::baselineCompareUrl($tenant),
ManagedEnvironmentLinks::operationsUrl($tenant),
ManagedEnvironmentLinks::providerConnectionsUrl($tenant),
ManagedEnvironmentResource::getUrl('index'),
@ -47,6 +48,7 @@
->and(ManagedEnvironmentLinks::requiredPermissionsUrl($tenant))->toEndWith('/required-permissions')
->and(ManagedEnvironmentLinks::diagnosticsUrl($tenant))->toEndWith('/diagnostics')
->and(ManagedEnvironmentLinks::accessScopesUrl($tenant))->toEndWith('/access-scopes')
->and(ManagedEnvironmentLinks::baselineCompareUrl($tenant))->toEndWith('/baseline-compare')
->and(ManagedEnvironmentLinks::providerConnectionsUrl($tenant))->toContain('/admin/provider-connections?environment_id='.(int) $tenant->getKey())
->and(OperationRunLinks::index($tenant))->toContain('/admin/workspaces/')
->and(OperationRunLinks::tenantlessView($run))->toContain('/admin/workspaces/');

View File

@ -41,6 +41,9 @@
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments'))->toBeTrue()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2'))->toBeFalse()
->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2'))->toBeTrue()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2/baseline-compare'))->toBeFalse()
->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2/baseline-compare'))->toBeTrue()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-compare-landing'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2/stored-reports'))->toBeFalse()
->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2/stored-reports'))->toBeTrue();
});

View File

@ -10,9 +10,9 @@
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Support\Auth\Capabilities;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
use Tests\Feature\Concerns\BuildsBaselineCompareMatrixFixtures;
uses(RefreshDatabase::class, BuildsBaselineCompareMatrixFixtures::class);
@ -69,7 +69,7 @@
->assertNotFound();
});
it('returns 403 for matrix tenant drilldowns when tenant view capability is missing', function (): void {
it('returns 404 for matrix tenant drilldowns when tenant view capability is missing', function (): void {
$fixture = $this->makeBaselineCompareMatrixFixture();
$resolver = \Mockery::mock(CapabilityResolver::class);
@ -77,7 +77,9 @@
$resolver->shouldReceive('can')->andReturnFalse();
app()->instance(CapabilityResolver::class, $resolver);
$this->actingAs($fixture['user']);
$this->actingAs($fixture['user'])->withSession([
WorkspaceContext::SESSION_KEY => (int) $fixture['workspace']->getKey(),
]);
$fixture['visibleTenant']->makeCurrent();
$query = CanonicalNavigationContext::forBaselineCompareMatrix(
@ -87,7 +89,7 @@
)->toQuery();
$this->get(BaselineCompareLanding::getUrl(parameters: $query, panel: 'admin', tenant: $fixture['visibleTenant']))
->assertForbidden();
->assertNotFound();
});
it('returns 403 for matrix finding drilldowns when findings view capability is missing', function (): void {
@ -132,10 +134,10 @@
$this->actingAs($fixture['user']);
$fixture['visibleTenant']->makeCurrent();
$component = Livewire::withQueryParams([
$component = baselineCompareLandingLivewire($fixture['visibleTenant'], [
'baseline_profile_id' => (int) $foreignProfile->getKey(),
'subject_key' => 'wifi-corp-profile',
])->test(BaselineCompareLanding::class);
]);
expect($component->instance()->openCompareMatrixUrl())
->toStartWith(BaselineProfileResource::compareMatrixUrl($fixture['profile']))

View File

@ -8,9 +8,13 @@
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(BaselineCompareLanding::getUrl(panel: 'admin').'?tenant='.urlencode((string) $tenant->external_id))
->get(BaselineCompareLanding::getUrl(tenant: $tenant, panel: 'admin'))
->assertOk();
$this->actingAs($user)
->get('/admin/baseline-compare-landing?tenant='.urlencode((string) $tenant->external_id))
->assertNotFound();
$this->actingAs($user)
->get('/admin/t/'.$tenant->external_id.'/drift-landing')
->assertNotFound();

View File

@ -1,28 +1,28 @@
<?php
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment;
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentOnboardingSession;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
use App\Models\RestoreRun;
use App\Models\StoredReport;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentOnboardingSession;
use App\Models\EnvironmentReview;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Evidence\EvidenceSnapshotService;
use App\Services\Graph\GraphClientInterface;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\EnvironmentReviews\EnvironmentReviewService;
use App\Services\Evidence\EvidenceSnapshotService;
use App\Services\Graph\GraphClientInterface;
use App\Services\Tenants\TenantActionPolicySurface;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
@ -818,6 +818,21 @@ function createUserWithTenant(
return [$user, $tenant];
}
function baselineCompareLandingLivewire(ManagedEnvironment $tenant, array $queryParams = []): mixed
{
$manager = \Livewire\Livewire::withHeaders([
'Referer' => \App\Support\ManagedEnvironmentLinks::baselineCompareUrl($tenant),
]);
if ($queryParams !== []) {
$manager = $manager->withQueryParams($queryParams);
}
return $manager->test(\App\Filament\Pages\BaselineCompareLanding::class, [
'environment' => $tenant,
]);
}
/**
* @return array{0: BaselineProfile, 1: BaselineSnapshot}
*/

View File

@ -16,6 +16,7 @@
'retired tenant resource detail' => ['/admin/tenants/tenant-123', AdminSurfaceScope::WorkspaceScoped],
'retired tenant panel route' => ['/admin/t/tenant-123', AdminSurfaceScope::WorkspaceScoped],
'workspace environment detail' => ['/admin/workspaces/acme/environments/tenant-123', AdminSurfaceScope::EnvironmentBound],
'baseline compare environment route' => ['/admin/workspaces/acme/environments/tenant-123/baseline-compare', AdminSurfaceScope::EnvironmentBound],
'tenant scoped evidence detail' => ['/admin/evidence/123', AdminSurfaceScope::EnvironmentScopedEvidence],
'evidence overview' => ['/admin/evidence/overview', AdminSurfaceScope::WorkspaceWideSurface],
'customer review workspace' => ['/admin/reviews/workspace', AdminSurfaceScope::WorkspaceWideSurface],