feat: harden provider connection authority resolution (339)
This commit is contained in:
parent
2fa468bdc7
commit
1313ced181
@ -21,8 +21,8 @@
|
|||||||
use App\Support\Badges\BadgeRenderer;
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\Filament\TablePaginationProfiles;
|
use App\Support\Filament\TablePaginationProfiles;
|
||||||
use App\Support\ManagedEnvironmentLinks;
|
use App\Support\ManagedEnvironmentLinks;
|
||||||
use App\Support\Navigation\WorkspaceHubNavigation;
|
|
||||||
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
||||||
|
use App\Support\Navigation\WorkspaceHubNavigation;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OperationRunType;
|
use App\Support\OperationRunType;
|
||||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||||
@ -270,6 +270,12 @@ private static function resolveTenantExternalIdFromLivewireRequest(): ?string
|
|||||||
// Ignore and fall back to referer.
|
// Ignore and fall back to referer.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$externalId = static::extractTenantExternalIdFromLivewireSnapshot();
|
||||||
|
|
||||||
|
if (is_string($externalId) && $externalId !== '') {
|
||||||
|
return $externalId;
|
||||||
|
}
|
||||||
|
|
||||||
$referer = request()->headers->get('referer');
|
$referer = request()->headers->get('referer');
|
||||||
|
|
||||||
if (! is_string($referer) || $referer === '') {
|
if (! is_string($referer) || $referer === '') {
|
||||||
@ -279,6 +285,56 @@ private static function resolveTenantExternalIdFromLivewireRequest(): ?string
|
|||||||
return static::extractTenantExternalIdFromUrl($referer);
|
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
|
private static function extractTenantExternalIdFromUrl(string $url): ?string
|
||||||
{
|
{
|
||||||
$query = parse_url($url, PHP_URL_QUERY);
|
$query = parse_url($url, PHP_URL_QUERY);
|
||||||
@ -341,14 +397,6 @@ private static function applyMembershipScope(Builder $query): Builder
|
|||||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
||||||
$user = auth()->user();
|
$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) {
|
if (! is_int($workspaceId) || ! $user instanceof User) {
|
||||||
return $query->whereRaw('1 = 0');
|
return $query->whereRaw('1 = 0');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,19 +9,34 @@
|
|||||||
use App\Support\Providers\ProviderConnectionType;
|
use App\Support\Providers\ProviderConnectionType;
|
||||||
use App\Support\Providers\ProviderConsentStatus;
|
use App\Support\Providers\ProviderConsentStatus;
|
||||||
use App\Support\Providers\ProviderReasonCodes;
|
use App\Support\Providers\ProviderReasonCodes;
|
||||||
|
use App\Support\Providers\ProviderVerificationStatus;
|
||||||
use App\Support\Providers\TargetScope\ProviderConnectionTargetScopeDescriptor;
|
use App\Support\Providers\TargetScope\ProviderConnectionTargetScopeDescriptor;
|
||||||
use App\Support\Providers\TargetScope\ProviderConnectionTargetScopeNormalizer;
|
use App\Support\Providers\TargetScope\ProviderConnectionTargetScopeNormalizer;
|
||||||
use App\Support\Providers\ProviderVerificationStatus;
|
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Livewire\Attributes\Locked;
|
||||||
|
|
||||||
class CreateProviderConnection extends CreateRecord
|
class CreateProviderConnection extends CreateRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = ProviderConnectionResource::class;
|
protected static string $resource = ProviderConnectionResource::class;
|
||||||
|
|
||||||
|
#[Locked]
|
||||||
|
public ?int $environmentId = null;
|
||||||
|
|
||||||
protected bool $shouldMakeDefault = false;
|
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
|
protected function mutateFormDataBeforeCreate(array $data): array
|
||||||
{
|
{
|
||||||
$tenant = $this->currentTenant();
|
$tenant = $this->currentTenant();
|
||||||
|
|||||||
@ -9,10 +9,10 @@
|
|||||||
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
|
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
|
||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use Filament\Facades\Filament;
|
|
||||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
use Illuminate\Auth\Access\Response;
|
use Illuminate\Auth\Access\Response;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Livewire\Livewire as LivewireFacade;
|
||||||
|
|
||||||
class ProviderConnectionPolicy
|
class ProviderConnectionPolicy
|
||||||
{
|
{
|
||||||
@ -210,14 +210,6 @@ private function currentWorkspace(User $user): ?Workspace
|
|||||||
{
|
{
|
||||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
$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)) {
|
if (! is_int($workspaceId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -237,33 +229,117 @@ private function currentWorkspace(User $user): ?Workspace
|
|||||||
|
|
||||||
private function resolveCreateTenant(Workspace $workspace): ?ManagedEnvironment
|
private function resolveCreateTenant(Workspace $workspace): ?ManagedEnvironment
|
||||||
{
|
{
|
||||||
$requestedEnvironmentId = request()->query('environment_id');
|
$requestedEnvironmentId = $this->requestedEnvironmentId();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if ($requestedEnvironmentId === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ManagedEnvironment::query()
|
return ManagedEnvironment::query()
|
||||||
->whereKey((int) $requestedEnvironmentId)
|
->whereKey($requestedEnvironmentId)
|
||||||
->where('workspace_id', (int) $workspace->getKey())
|
->where('workspace_id', (int) $workspace->getKey())
|
||||||
->first();
|
->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
|
private function tenantForConnection(ProviderConnection $connection): ?ManagedEnvironment
|
||||||
{
|
{
|
||||||
if ($connection->relationLoaded('tenant') && $connection->tenant instanceof ManagedEnvironment) {
|
if ($connection->relationLoaded('tenant') && $connection->tenant instanceof ManagedEnvironment) {
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
use App\Filament\Resources\ProviderConnectionResource\Pages\CreateProviderConnection;
|
use App\Filament\Resources\ProviderConnectionResource\Pages\CreateProviderConnection;
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
use App\Models\ProviderConnection;
|
|
||||||
use App\Models\ManagedEnvironment;
|
use App\Models\ManagedEnvironment;
|
||||||
|
use App\Models\ProviderConnection;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@ -76,6 +76,9 @@
|
|||||||
Filament::setTenant($tenant, true);
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
|
->withQueryParams([
|
||||||
|
'environment_id' => (int) $tenant->getKey(),
|
||||||
|
])
|
||||||
->test(CreateProviderConnection::class)
|
->test(CreateProviderConnection::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
'display_name' => 'Audit target scope connection',
|
'display_name' => 'Audit target scope connection',
|
||||||
|
|||||||
@ -15,7 +15,11 @@
|
|||||||
$tenant->makeCurrent();
|
$tenant->makeCurrent();
|
||||||
Filament::setTenant($tenant, true);
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(CreateProviderConnection::class)
|
$component = Livewire::withQueryParams([
|
||||||
|
'environment_id' => (int) $tenant->getKey(),
|
||||||
|
])->test(CreateProviderConnection::class);
|
||||||
|
|
||||||
|
$component
|
||||||
->fillForm([
|
->fillForm([
|
||||||
'display_name' => 'MVP Scope Connection',
|
'display_name' => 'MVP Scope Connection',
|
||||||
'entra_tenant_id' => (string) fake()->uuid(),
|
'entra_tenant_id' => (string) fake()->uuid(),
|
||||||
|
|||||||
@ -81,6 +81,9 @@
|
|||||||
Filament::setTenant($tenant, true);
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
|
->withQueryParams([
|
||||||
|
'environment_id' => (int) $tenant->getKey(),
|
||||||
|
])
|
||||||
->test(CreateProviderConnection::class)
|
->test(CreateProviderConnection::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
'display_name' => 'Missing target scope',
|
'display_name' => 'Missing target scope',
|
||||||
|
|||||||
@ -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();
|
||||||
|
});
|
||||||
@ -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();
|
||||||
|
});
|
||||||
@ -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();
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user