From 350ef1dd4861e2c863dc50d5fb1e17570c479d87 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 20 Mar 2026 11:14:08 +0000 Subject: [PATCH 1/9] filter query to authorized collections, don't require all collections to be authorized --- src/Fieldtypes/Entries.php | 37 ++++++++--------- src/Query/Scopes/Filters/Collection.php | 25 ++++++++++- .../Fieldtypes/RelationshipFieldtypeTest.php | 41 +++++++++++++++++-- 3 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 293c2b4c588..15d4fa94758 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -134,15 +134,24 @@ protected function configFieldItems(): array public function getIndexItems($request) { $configuredCollections = $this->getConfiguredCollections(); - $requestedCollections = $this->getRequestedCollections($request, $configuredCollections); - $this->authorizeCollectionAccess($requestedCollections); + $this->authorizeCollectionAccess($configuredCollections); $query = $this->getIndexQuery($request); $filters = $request->filters; if (! isset($filters['collection'])) { - $query->whereIn('collection', $configuredCollections); + $user = User::current(); + + $query->whereIn( + 'collection', + collect($configuredCollections) + ->map(fn (string $collectionHandle) => Collection::findByHandle($collectionHandle)) + ->filter() + ->filter(fn ($collection) => $user->can('view', $collection)) + ->map->handle() + ->all() + ); } if ($blueprints = $this->config('blueprints')) { @@ -162,28 +171,16 @@ public function getIndexItems($request) return $paginate ? $results->setCollection($items) : $items; } - private function getRequestedCollections($request, $configuredCollections) - { - $filteredCollections = collect($request->input('filters.collection.collections', [])) - ->filter() - ->values() - ->all(); - - return empty($filteredCollections) ? $configuredCollections : $filteredCollections; - } - private function authorizeCollectionAccess($collections) { $user = User::current(); - collect($collections)->each(function ($collectionHandle) use ($user) { - $collection = Collection::findByHandle($collectionHandle); + $authorizedCollections = collect($collections) + ->map(fn (string $collectionHandle) => Collection::findByHandle($collectionHandle)) + ->filter() + ->filter(fn ($collection) => $user->can('view', $collection)); - throw_if( - ! $collection || ! $user->can('view', $collection), - new AuthorizationException - ); - }); + throw_if($authorizedCollections->isEmpty(), new AuthorizationException); } public function getResourceCollection($request, $items) diff --git a/src/Query/Scopes/Filters/Collection.php b/src/Query/Scopes/Filters/Collection.php index ddfa4cf9b04..4c265f52b52 100644 --- a/src/Query/Scopes/Filters/Collection.php +++ b/src/Query/Scopes/Filters/Collection.php @@ -2,7 +2,9 @@ namespace Statamic\Query\Scopes\Filters; +use Statamic\Exceptions\AuthorizationException; use Statamic\Facades; +use Statamic\Facades\User; use Statamic\Query\Scopes\Filter; class Collection extends Filter @@ -29,6 +31,8 @@ public function fieldItems() public function apply($query, $values) { + $this->authorizeCollectionAccess($values['collections']); + $query->whereIn('collection', $values['collections']); } @@ -44,8 +48,25 @@ public function visibleTo($key) protected function options() { - return collect($this->context['collections'])->mapWithKeys(function ($collection) { - return [$collection => Facades\Collection::findByHandle($collection)->title()]; + $user = User::current(); + + return collect($this->context['collections']) + ->map(fn ($collection) => Facades\Collection::findByHandle($collection)) + ->filter(fn ($collection) => $user->can('view', $collection)) + ->mapWithKeys(fn ($collection) => [$collection->handle() => $collection->title()]); + } + + private function authorizeCollectionAccess(array $collections) + { + $user = User::current(); + + collect($collections)->each(function ($collectionHandle) use ($user) { + $collection = Facades\Collection::findByHandle($collectionHandle); + + throw_if( + ! $collection || ! $user->can('view', $collection), + new AuthorizationException + ); }); } } diff --git a/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php b/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php index 3d9504855c7..399bb2d0cfd 100644 --- a/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php +++ b/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php @@ -61,8 +61,40 @@ public function it_filters_entries_by_query_scopes() } #[Test] - public function it_denies_access_to_entries_when_theres_a_collection_the_user_cannot_view() + public function it_limits_access_to_entries_from_collections_the_user_can_view() { + Collection::make('pages')->save(); + Entry::make()->collection('pages')->slug('home')->data(['title' => 'Home'])->save(); + + Collection::make('secret')->save(); + Entry::make()->collection('secret')->slug('secret-one')->data(['title' => 'Secret One'])->save(); + + $this->setTestRoles(['test' => ['access cp', 'view pages entries']]); + $user = User::make()->assignRole('test')->save(); + + $config = base64_encode(json_encode([ + 'type' => 'entries', + 'collections' => ['pages', 'secret'], + ])); + + $this + ->actingAs($user) + ->getJson("/cp/fieldtypes/relationship?config={$config}") + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJson([ + 'data' => [ + ['slug' => 'home'], + ], + ]); + } + + #[Test] + public function it_denies_access_to_entries_when_user_cannot_view_any_of_the_collections() + { + Collection::make('pages')->save(); + Entry::make()->collection('pages')->slug('home')->data(['title' => 'Home'])->save(); + Collection::make('secret')->save(); Entry::make()->collection('secret')->slug('secret-one')->data(['title' => 'Secret One'])->save(); @@ -71,7 +103,7 @@ public function it_denies_access_to_entries_when_theres_a_collection_the_user_ca $config = base64_encode(json_encode([ 'type' => 'entries', - 'collections' => ['secret'], + 'collections' => ['pages', 'secret'], ])); $this @@ -81,8 +113,11 @@ public function it_denies_access_to_entries_when_theres_a_collection_the_user_ca } #[Test] - public function it_forbids_access_to_entries_when_filters_target_a_collection_the_user_cannot_view() + public function it_forbids_access_to_entries_when_filters_target_collections_the_user_cannot_view() { + Collection::make('pages')->save(); + Entry::make()->collection('pages')->slug('home')->data(['title' => 'Home'])->save(); + Collection::make('secret')->save(); Entry::make()->collection('test')->slug('apple')->data(['title' => 'Apple'])->save(); Entry::make()->collection('secret')->slug('secret-one')->data(['title' => 'Secret One'])->save(); From 310a7665ef013b189e06299e33aacdb41404b03c Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 20 Mar 2026 11:47:12 +0000 Subject: [PATCH 2/9] types. --- src/Fieldtypes/Entries.php | 2 +- src/Query/Scopes/Filters/Collection.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 15d4fa94758..6b7c5bac118 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -171,7 +171,7 @@ public function getIndexItems($request) return $paginate ? $results->setCollection($items) : $items; } - private function authorizeCollectionAccess($collections) + private function authorizeCollectionAccess(array $collections): void { $user = User::current(); diff --git a/src/Query/Scopes/Filters/Collection.php b/src/Query/Scopes/Filters/Collection.php index 4c265f52b52..ddfffa612b5 100644 --- a/src/Query/Scopes/Filters/Collection.php +++ b/src/Query/Scopes/Filters/Collection.php @@ -56,11 +56,11 @@ protected function options() ->mapWithKeys(fn ($collection) => [$collection->handle() => $collection->title()]); } - private function authorizeCollectionAccess(array $collections) + private function authorizeCollectionAccess(array $collections): void { $user = User::current(); - collect($collections)->each(function ($collectionHandle) use ($user) { + collect($collections)->each(function (string $collectionHandle) use ($user) { $collection = Facades\Collection::findByHandle($collectionHandle); throw_if( From 0d2a896f6f4d3a5542f6d2dd578a4d7290e7f480 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 20 Mar 2026 11:47:35 +0000 Subject: [PATCH 3/9] copy changes to the terms fieldtype --- src/Fieldtypes/Terms.php | 37 ++++++++----------- .../Fieldtypes/RelationshipFieldtypeTest.php | 28 ++++++++------ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/Fieldtypes/Terms.php b/src/Fieldtypes/Terms.php index 3408672388c..684316e1f49 100644 --- a/src/Fieldtypes/Terms.php +++ b/src/Fieldtypes/Terms.php @@ -263,9 +263,7 @@ public function getIndexItems($request) return collect(); } - $this->authorizeTaxonomyAccess( - $this->getRequestedTaxonomies($request, $this->getConfiguredTaxonomies()) - ); + $this->authorizeTaxonomyAccess($this->getConfiguredTaxonomies()); $query = $this->getIndexQuery($request); @@ -276,25 +274,16 @@ public function getIndexItems($request) return $request->boolean('paginate', true) ? $query->paginate() : $query->get(); } - private function getRequestedTaxonomies($request, $configuredTaxonomies) - { - $requestedTaxonomies = collect($request->taxonomies)->filter()->values()->all(); - - return empty($requestedTaxonomies) ? $configuredTaxonomies : $requestedTaxonomies; - } - - private function authorizeTaxonomyAccess($taxonomies) + private function authorizeTaxonomyAccess(array $taxonomies): void { $user = User::current(); - collect($taxonomies)->each(function ($taxonomyHandle) use ($user) { - $taxonomy = Taxonomy::findByHandle($taxonomyHandle); + $authorizedTaxonomies = collect($taxonomies) + ->map(fn (string $taxonomyHandle) => Taxonomy::findByHandle($taxonomyHandle)) + ->filter() + ->filter(fn ($taxonomy) => $user->can('view', $taxonomy)); - throw_if( - ! $taxonomy || ! $user->can('view', $taxonomy), - new AuthorizationException - ); - }); + throw_if($authorizedTaxonomies->isEmpty(), new AuthorizationException); } public function getResourceCollection($request, $items) @@ -434,10 +423,16 @@ protected function getColumns() protected function getIndexQuery($request) { $query = Term::query(); + $user = User::current(); - if ($taxonomies = $request->taxonomies) { - $query->whereIn('taxonomy', $taxonomies); - } + $taxonomies = collect($request->taxonomies ?? $this->getConfiguredTaxonomies()) + ->map(fn (string $taxonomyHandle) => Taxonomy::findByHandle($taxonomyHandle)) + ->filter() + ->filter(fn ($collection) => $user->can('view', $collection)) + ->map->handle() + ->all(); + + $query->whereIn('taxonomy', $taxonomies); if ($search = $request->search) { $query->where('title', 'like', '%'.$search.'%'); diff --git a/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php b/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php index 399bb2d0cfd..dba063dba16 100644 --- a/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php +++ b/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php @@ -142,46 +142,52 @@ public function it_forbids_access_to_entries_when_filters_target_collections_the } #[Test] - public function it_forbids_access_to_terms_when_config_contains_a_taxonomy_the_user_cannot_view() + public function it_limits_access_to_terms_from_taxonomies_the_user_can_view() { + Taxonomy::make('topics')->save(); Taxonomy::make('secret')->save(); + Term::make('public')->taxonomy('topics')->data([])->save(); Term::make('internal')->taxonomy('secret')->data([])->save(); - $this->setTestRoles(['test' => ['access cp']]); + $this->setTestRoles(['test' => ['access cp', 'view topics terms']]); $user = User::make()->assignRole('test')->save(); $config = base64_encode(json_encode([ 'type' => 'terms', - 'taxonomies' => ['secret'], + 'taxonomies' => ['topics', 'secret'], ])); $this ->actingAs($user) - ->getJson("/cp/fieldtypes/relationship?config={$config}&taxonomies[0]=secret") - ->assertForbidden(); + ->getJson("/cp/fieldtypes/relationship?config={$config}&taxonomies[0]=topics&taxonomies[1]=secret") + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJson([ + 'data' => [ + ['slug' => 'public'], + ], + ]); } #[Test] - public function it_forbids_access_to_terms_when_requested_taxonomy_is_forbidden() + public function it_forbids_access_to_terms_when_the_user_cannot_view_any_of_the_taxonomies() { Taxonomy::make('topics')->save(); Taxonomy::make('secret')->save(); Term::make('public')->taxonomy('topics')->data([])->save(); Term::make('internal')->taxonomy('secret')->data([])->save(); - $this->setTestRoles([ - 'test' => ['access cp', 'view topics terms'], - ]); + $this->setTestRoles(['test' => ['access cp']]); $user = User::make()->assignRole('test')->save(); $config = base64_encode(json_encode([ 'type' => 'terms', - 'taxonomies' => ['topics'], + 'taxonomies' => ['topics', 'secret'], ])); $this ->actingAs($user) - ->getJson("/cp/fieldtypes/relationship?config={$config}&taxonomies[0]=secret") + ->getJson("/cp/fieldtypes/relationship?config={$config}&taxonomies[0]=topics&taxonomies[1]=secret") ->assertForbidden(); } } From e92b7ae9d487b7eb5d26b00a35c0acf4eaddad14 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Mar 2026 10:31:26 -0400 Subject: [PATCH 4/9] clarity --- src/Fieldtypes/Terms.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fieldtypes/Terms.php b/src/Fieldtypes/Terms.php index 684316e1f49..6a2d92e4243 100644 --- a/src/Fieldtypes/Terms.php +++ b/src/Fieldtypes/Terms.php @@ -428,7 +428,7 @@ protected function getIndexQuery($request) $taxonomies = collect($request->taxonomies ?? $this->getConfiguredTaxonomies()) ->map(fn (string $taxonomyHandle) => Taxonomy::findByHandle($taxonomyHandle)) ->filter() - ->filter(fn ($collection) => $user->can('view', $collection)) + ->filter(fn ($taxonomy) => $user->can('view', $taxonomy)) ->map->handle() ->all(); From 8ad193b03803f60da7f37387870bc36fd2148266 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Mar 2026 10:35:08 -0400 Subject: [PATCH 5/9] fix missing filter in scope, and consolidate double filters. --- src/Fieldtypes/Entries.php | 3 +-- src/Fieldtypes/Terms.php | 3 +-- src/Query/Scopes/Filters/Collection.php | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 6b7c5bac118..0117883e560 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -147,8 +147,7 @@ public function getIndexItems($request) 'collection', collect($configuredCollections) ->map(fn (string $collectionHandle) => Collection::findByHandle($collectionHandle)) - ->filter() - ->filter(fn ($collection) => $user->can('view', $collection)) + ->filter(fn ($collection) => $collection && $user->can('view', $collection)) ->map->handle() ->all() ); diff --git a/src/Fieldtypes/Terms.php b/src/Fieldtypes/Terms.php index 6a2d92e4243..ee8aa18568e 100644 --- a/src/Fieldtypes/Terms.php +++ b/src/Fieldtypes/Terms.php @@ -427,8 +427,7 @@ protected function getIndexQuery($request) $taxonomies = collect($request->taxonomies ?? $this->getConfiguredTaxonomies()) ->map(fn (string $taxonomyHandle) => Taxonomy::findByHandle($taxonomyHandle)) - ->filter() - ->filter(fn ($taxonomy) => $user->can('view', $taxonomy)) + ->filter(fn ($taxonomy) => $taxonomy && $user->can('view', $taxonomy)) ->map->handle() ->all(); diff --git a/src/Query/Scopes/Filters/Collection.php b/src/Query/Scopes/Filters/Collection.php index ddfffa612b5..169b1a58c75 100644 --- a/src/Query/Scopes/Filters/Collection.php +++ b/src/Query/Scopes/Filters/Collection.php @@ -52,7 +52,7 @@ protected function options() return collect($this->context['collections']) ->map(fn ($collection) => Facades\Collection::findByHandle($collection)) - ->filter(fn ($collection) => $user->can('view', $collection)) + ->filter(fn ($collection) => $collection && $user->can('view', $collection)) ->mapWithKeys(fn ($collection) => [$collection->handle() => $collection->title()]); } From 6f8028a69e47f17b0e4dd10ca6460e29e11f56c5 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Mar 2026 12:16:50 -0400 Subject: [PATCH 6/9] remove taxonomy-from-query-param logic. as far as i can tell this is completely unused. --- src/Fieldtypes/Terms.php | 6 ++---- tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Fieldtypes/Terms.php b/src/Fieldtypes/Terms.php index ee8aa18568e..b7eaf8c2f45 100644 --- a/src/Fieldtypes/Terms.php +++ b/src/Fieldtypes/Terms.php @@ -300,9 +300,7 @@ protected function getBlueprint($request) protected function getFirstTaxonomyFromRequest($request) { - return $request->taxonomies - ? Facades\Taxonomy::findByHandle($request->taxonomies[0]) - : Facades\Taxonomy::all()->first(); + return Facades\Taxonomy::all()->first(); } public function getSortColumn($request) @@ -425,7 +423,7 @@ protected function getIndexQuery($request) $query = Term::query(); $user = User::current(); - $taxonomies = collect($request->taxonomies ?? $this->getConfiguredTaxonomies()) + $taxonomies = collect($this->getConfiguredTaxonomies()) ->map(fn (string $taxonomyHandle) => Taxonomy::findByHandle($taxonomyHandle)) ->filter(fn ($taxonomy) => $taxonomy && $user->can('view', $taxonomy)) ->map->handle() diff --git a/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php b/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php index dba063dba16..f9354987962 100644 --- a/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php +++ b/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php @@ -159,7 +159,7 @@ public function it_limits_access_to_terms_from_taxonomies_the_user_can_view() $this ->actingAs($user) - ->getJson("/cp/fieldtypes/relationship?config={$config}&taxonomies[0]=topics&taxonomies[1]=secret") + ->getJson("/cp/fieldtypes/relationship?config={$config}") ->assertOk() ->assertJsonCount(1, 'data') ->assertJson([ @@ -187,7 +187,7 @@ public function it_forbids_access_to_terms_when_the_user_cannot_view_any_of_the_ $this ->actingAs($user) - ->getJson("/cp/fieldtypes/relationship?config={$config}&taxonomies[0]=topics&taxonomies[1]=secret") + ->getJson("/cp/fieldtypes/relationship?config={$config}") ->assertForbidden(); } } From f9541c29585c773ac7b01e42ea7eb1ce66a8de9b Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Mar 2026 12:23:19 -0400 Subject: [PATCH 7/9] oops. didnt get the correct first taxonomy. now it does by mirroring logic from entries fieldtype. --- src/Fieldtypes/Terms.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Fieldtypes/Terms.php b/src/Fieldtypes/Terms.php index b7eaf8c2f45..424675af64d 100644 --- a/src/Fieldtypes/Terms.php +++ b/src/Fieldtypes/Terms.php @@ -300,7 +300,13 @@ protected function getBlueprint($request) protected function getFirstTaxonomyFromRequest($request) { - return Facades\Taxonomy::all()->first(); + $taxonomies = $this->getConfiguredTaxonomies(); + + $taxonomy = Taxonomy::findByHandle($taxonomyHandle = Arr::first($taxonomies)); + + throw_if(! $taxonomy, new TaxonomyNotFoundException($taxonomyHandle)); + + return $taxonomy; } public function getSortColumn($request) From 41dc646ea616e7424acd2a3051913641bebd6c4c Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Mar 2026 12:24:51 -0400 Subject: [PATCH 8/9] remove unused collection+entry from test --- tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php b/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php index f9354987962..3bb3c04d1a4 100644 --- a/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php +++ b/tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php @@ -115,9 +115,6 @@ public function it_denies_access_to_entries_when_user_cannot_view_any_of_the_col #[Test] public function it_forbids_access_to_entries_when_filters_target_collections_the_user_cannot_view() { - Collection::make('pages')->save(); - Entry::make()->collection('pages')->slug('home')->data(['title' => 'Home'])->save(); - Collection::make('secret')->save(); Entry::make()->collection('test')->slug('apple')->data(['title' => 'Apple'])->save(); Entry::make()->collection('secret')->slug('secret-one')->data(['title' => 'Secret One'])->save(); From 66c21a0f3800bd83c307d39cb5f2abbf2b54408f Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Mar 2026 12:46:20 -0400 Subject: [PATCH 9/9] consolidate filters --- src/Fieldtypes/Entries.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 0117883e560..c2196dcf480 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -176,8 +176,7 @@ private function authorizeCollectionAccess(array $collections): void $authorizedCollections = collect($collections) ->map(fn (string $collectionHandle) => Collection::findByHandle($collectionHandle)) - ->filter() - ->filter(fn ($collection) => $user->can('view', $collection)); + ->filter(fn ($collection) => $collection && $user->can('view', $collection)); throw_if($authorizedCollections->isEmpty(), new AuthorizationException); }