feat: harden provider connection authority resolution (339)

This commit is contained in:
Ahmed Darrazi 2026-05-31 13:06:07 +02:00
parent 2fa468bdc7
commit 1313ced181
9 changed files with 452 additions and 39 deletions

View File

@ -21,8 +21,8 @@
use App\Support\Badges\BadgeRenderer;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\ManagedEnvironmentLinks;
use App\Support\Navigation\WorkspaceHubNavigation;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\Navigation\WorkspaceHubNavigation;
use App\Support\OperationRunLinks;
use App\Support\OperationRunType;
use App\Support\OpsUx\OpsUxBrowserEvents;
@ -270,6 +270,12 @@ private static function resolveTenantExternalIdFromLivewireRequest(): ?string
// Ignore and fall back to referer.
}
$externalId = static::extractTenantExternalIdFromLivewireSnapshot();
if (is_string($externalId) && $externalId !== '') {
return $externalId;
}
$referer = request()->headers->get('referer');
if (! is_string($referer) || $referer === '') {
@ -279,6 +285,56 @@ private static function resolveTenantExternalIdFromLivewireRequest(): ?string
return static::extractTenantExternalIdFromUrl($referer);
}
private static function extractTenantExternalIdFromLivewireSnapshot(): ?string
{
$snapshotJson = request()->input('components.0.snapshot');
if (! is_string($snapshotJson) || $snapshotJson === '') {
return null;
}
$snapshot = json_decode($snapshotJson, true);
if (! is_array($snapshot)) {
return null;
}
$componentName = data_get($snapshot, 'memo.name');
if (! is_string($componentName) || $componentName !== Pages\CreateProviderConnection::class) {
return null;
}
$environmentId = data_get($snapshot, 'data.environmentId');
if (is_array($environmentId) || filter_var($environmentId, FILTER_VALIDATE_INT) === false) {
return null;
}
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if (! is_int($workspaceId)) {
return null;
}
$tenant = ManagedEnvironment::query()
->whereKey((int) $environmentId)
->where('workspace_id', $workspaceId)
->first();
if (! $tenant instanceof ManagedEnvironment) {
return null;
}
$user = request()->user();
if ($user instanceof User && ! $user->canAccessTenant($tenant)) {
return null;
}
return (string) $tenant->slug;
}
private static function extractTenantExternalIdFromUrl(string $url): ?string
{
$query = parse_url($url, PHP_URL_QUERY);
@ -341,14 +397,6 @@ private static function applyMembershipScope(Builder $query): Builder
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
$user = auth()->user();
if (! is_int($workspaceId)) {
$tenant = static::resolveTenantContextForCurrentPanel();
if ($tenant instanceof ManagedEnvironment) {
$workspaceId = (int) $tenant->workspace_id;
}
}
if (! is_int($workspaceId) || ! $user instanceof User) {
return $query->whereRaw('1 = 0');
}

View File

@ -9,19 +9,34 @@
use App\Support\Providers\ProviderConnectionType;
use App\Support\Providers\ProviderConsentStatus;
use App\Support\Providers\ProviderReasonCodes;
use App\Support\Providers\ProviderVerificationStatus;
use App\Support\Providers\TargetScope\ProviderConnectionTargetScopeDescriptor;
use App\Support\Providers\TargetScope\ProviderConnectionTargetScopeNormalizer;
use App\Support\Providers\ProviderVerificationStatus;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Locked;
class CreateProviderConnection extends CreateRecord
{
protected static string $resource = ProviderConnectionResource::class;
#[Locked]
public ?int $environmentId = null;
protected bool $shouldMakeDefault = false;
public function mount(): void
{
$environmentId = request()->query('environment_id');
if (! is_array($environmentId) && filter_var($environmentId, FILTER_VALIDATE_INT) !== false) {
$this->environmentId = (int) $environmentId;
}
parent::mount();
}
protected function mutateFormDataBeforeCreate(array $data): array
{
$tenant = $this->currentTenant();

View File

@ -9,10 +9,10 @@
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Support\Auth\Capabilities;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire as LivewireFacade;
class ProviderConnectionPolicy
{
@ -210,14 +210,6 @@ private function currentWorkspace(User $user): ?Workspace
{
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if (! is_int($workspaceId)) {
$filamentTenant = Filament::getTenant();
if ($filamentTenant instanceof ManagedEnvironment) {
$workspaceId = (int) $filamentTenant->workspace_id;
}
}
if (! is_int($workspaceId)) {
return null;
}
@ -237,33 +229,117 @@ private function currentWorkspace(User $user): ?Workspace
private function resolveCreateTenant(Workspace $workspace): ?ManagedEnvironment
{
$requestedEnvironmentId = request()->query('environment_id');
if (! is_numeric($requestedEnvironmentId)) {
$lastEnvironmentId = app(WorkspaceContext::class)->lastEnvironmentId(request());
if (is_int($lastEnvironmentId)) {
return ManagedEnvironment::query()
->whereKey($lastEnvironmentId)
->where('workspace_id', (int) $workspace->getKey())
->first();
}
$filamentTenant = Filament::getTenant();
if ($filamentTenant instanceof ManagedEnvironment && (int) $filamentTenant->workspace_id === (int) $workspace->getKey()) {
return $filamentTenant;
}
$requestedEnvironmentId = $this->requestedEnvironmentId();
if ($requestedEnvironmentId === null) {
return null;
}
return ManagedEnvironment::query()
->whereKey((int) $requestedEnvironmentId)
->whereKey($requestedEnvironmentId)
->where('workspace_id', (int) $workspace->getKey())
->first();
}
private function requestedEnvironmentId(): ?int
{
$environmentId = request()->query('environment_id');
if (is_numeric($environmentId)) {
return (int) $environmentId;
}
if (is_array($environmentId)) {
return null;
}
try {
$resolved = $this->extractEnvironmentIdFromLivewireSnapshot();
if ($resolved !== null) {
return $resolved;
}
} catch (\Throwable) {
// Ignore and fall back to originalUrl() parsing.
}
try {
$url = LivewireFacade::originalUrl();
$resolved = $this->extractEnvironmentIdFromUrl($url);
if ($resolved !== null) {
return $resolved;
}
} catch (\Throwable) {
// Ignore and fall back to referer header.
}
$referer = request()->headers->get('referer');
if (! is_string($referer) || $referer === '') {
return null;
}
return $this->extractEnvironmentIdFromUrl($referer);
}
private function extractEnvironmentIdFromLivewireSnapshot(): ?int
{
if (! request()->headers->has('x-livewire') && ! request()->headers->has('x-livewire-navigate')) {
return null;
}
$snapshotJson = request()->input('components.0.snapshot');
if (! is_string($snapshotJson) || $snapshotJson === '') {
return null;
}
$snapshot = json_decode($snapshotJson, true);
if (! is_array($snapshot)) {
return null;
}
$componentName = data_get($snapshot, 'memo.name');
if (! is_string($componentName) || $componentName !== \App\Filament\Resources\ProviderConnectionResource\Pages\CreateProviderConnection::class) {
return null;
}
$environmentId = data_get($snapshot, 'data.environmentId');
if (is_array($environmentId) || filter_var($environmentId, FILTER_VALIDATE_INT) === false) {
return null;
}
return (int) $environmentId;
}
private function extractEnvironmentIdFromUrl(?string $url): ?int
{
if (! is_string($url) || $url === '') {
return null;
}
$query = parse_url($url, PHP_URL_QUERY);
if (! is_string($query) || $query === '') {
return null;
}
parse_str($query, $params);
$environmentId = $params['environment_id'] ?? null;
if (is_array($environmentId) || filter_var($environmentId, FILTER_VALIDATE_INT) === false) {
return null;
}
return (int) $environmentId;
}
private function tenantForConnection(ProviderConnection $connection): ?ManagedEnvironment
{
if ($connection->relationLoaded('tenant') && $connection->tenant instanceof ManagedEnvironment) {

View File

@ -4,8 +4,8 @@
use App\Filament\Resources\ProviderConnectionResource\Pages\CreateProviderConnection;
use App\Models\AuditLog;
use App\Models\ProviderConnection;
use App\Models\ManagedEnvironment;
use App\Models\ProviderConnection;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -76,6 +76,9 @@
Filament::setTenant($tenant, true);
Livewire::actingAs($user)
->withQueryParams([
'environment_id' => (int) $tenant->getKey(),
])
->test(CreateProviderConnection::class)
->fillForm([
'display_name' => 'Audit target scope connection',

View File

@ -15,7 +15,11 @@
$tenant->makeCurrent();
Filament::setTenant($tenant, true);
Livewire::test(CreateProviderConnection::class)
$component = Livewire::withQueryParams([
'environment_id' => (int) $tenant->getKey(),
])->test(CreateProviderConnection::class);
$component
->fillForm([
'display_name' => 'MVP Scope Connection',
'entra_tenant_id' => (string) fake()->uuid(),

View File

@ -81,6 +81,9 @@
Filament::setTenant($tenant, true);
Livewire::actingAs($user)
->withQueryParams([
'environment_id' => (int) $tenant->getKey(),
])
->test(CreateProviderConnection::class)
->fillForm([
'display_name' => 'Missing target scope',

View File

@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\Workspace;
use App\Policies\ProviderConnectionPolicy;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Auth\Access\Response;
use Illuminate\Http\Request;
it('ScopeHardening requires explicit environment_id for ProviderConnectionPolicy::create even if a remembered environment exists', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-env-remembered',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
$this->actingAs($user);
session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $environment->workspace_id => (int) $environment->getKey(),
]);
/** @var ProviderConnectionPolicy $policy */
$policy = app(ProviderConnectionPolicy::class);
$result = $policy->create($user);
expect($result)->toBeInstanceOf(Response::class);
expect($result->denied())->toBeTrue();
});
it('ScopeHardening requires explicit environment_id for ProviderConnectionPolicy::create even if Filament tenant is set', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-env-filament',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
Filament::setTenant($environment, true);
$this->actingAs($user);
session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id);
/** @var ProviderConnectionPolicy $policy */
$policy = app(ProviderConnectionPolicy::class);
$result = $policy->create($user);
expect($result)->toBeInstanceOf(Response::class);
expect($result->denied())->toBeTrue();
});
it('ScopeHardening ignores legacy query aliases for ProviderConnectionPolicy::create authority', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-env-alias',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
$request = Request::create('/admin/provider-connections/create', 'GET', [
'managed_environment_id' => (int) $environment->getKey(),
]);
$this->app->instance('request', $request);
$this->actingAs($user);
session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $environment->workspace_id => (int) $environment->getKey(),
]);
/** @var ProviderConnectionPolicy $policy */
$policy = app(ProviderConnectionPolicy::class);
$result = $policy->create($user);
expect($result)->toBeInstanceOf(Response::class);
expect($result->denied())->toBeTrue();
});
it('ScopeHardening denies ProviderConnectionPolicy::create for environment_id that belongs to another workspace', function (): void {
$environmentA = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-env-a',
]);
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner');
$workspaceB = Workspace::factory()->create();
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspaceB->getKey(),
'external_id' => 'spec339-scopehardening-env-b',
]);
$request = Request::create('/admin/provider-connections/create', 'GET', [
'environment_id' => (int) $environmentB->getKey(),
]);
$this->app->instance('request', $request);
$this->actingAs($user);
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
/** @var ProviderConnectionPolicy $policy */
$policy = app(ProviderConnectionPolicy::class);
$result = $policy->create($user);
expect($result)->toBeInstanceOf(Response::class);
expect($result->denied())->toBeTrue();
});
it('ScopeHardening returns 403 (not 404) for the create page when environment_id is missing (UI entry behavior)', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-create-page',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
])
->get('/admin/provider-connections/create')
->assertForbidden();
});
it('ScopeHardening returns 404 for the create page when environment_id belongs to another workspace (UI entry behavior)', function (): void {
$environmentA = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-create-page-a',
]);
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner');
$workspaceB = Workspace::factory()->create();
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspaceB->getKey(),
'external_id' => 'spec339-scopehardening-create-page-b',
]);
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $environmentA->workspace_id,
])
->get('/admin/provider-connections/create?environment_id='.(int) $environmentB->getKey())
->assertNotFound();
});
it('ScopeHardening returns 403 (not 404) for the create page when legacy alias is provided (UI entry behavior)', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-create-page-alias',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
])
->get('/admin/provider-connections/create?managed_environment_id='.(int) $environment->getKey())
->assertForbidden();
});
it('ScopeHardening returns 403 (not 404) for the create page even if a remembered environment exists (UI entry behavior)', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-create-page-remembered',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
(string) $environment->workspace_id => (int) $environment->getKey(),
],
])
->get('/admin/provider-connections/create')
->assertForbidden();
});
it('ScopeHardening returns 403 (not 404) for the create page even if Filament tenant is set (UI entry behavior)', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-create-page-filament',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
Filament::setTenant($environment, true);
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
])
->get('/admin/provider-connections/create')
->assertForbidden();
});

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Policies\ProviderConnectionPolicy;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Auth\Access\Response;
it('ScopeHardening does not derive workspace authority from Filament tenant in ProviderConnectionPolicy', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-policy-env',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
expect($user)->toBeInstanceOf(User::class);
session()->forget(WorkspaceContext::SESSION_KEY);
Filament::setTenant($environment, true);
/** @var ProviderConnectionPolicy $policy */
$policy = app(ProviderConnectionPolicy::class);
$result = $policy->viewAny($user);
expect($result)->toBeInstanceOf(Response::class);
expect($result->denied())->toBeTrue();
});

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use App\Filament\Resources\ProviderConnectionResource\Pages\ListProviderConnections;
use App\Models\ManagedEnvironment;
use App\Models\ProviderConnection;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Livewire\Livewire;
it('ScopeHardening does not infer provider connection list workspace from Filament tenant when no workspace is selected', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'external_id' => 'spec339-scopehardening-resource-env',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'display_name' => 'Spec339 ScopeHardening Connection',
'provider' => 'microsoft',
]);
$this->actingAs($user);
session()->forget(WorkspaceContext::SESSION_KEY);
Filament::setTenant($environment, true);
$component = Livewire::test(ListProviderConnections::class);
expect($component->instance())->toBeNull();
});