Skip to content

Commit 7c820bb

Browse files
authored
[5.x] Relationship endpoint authorization (statamic#14254)
1 parent a9f513b commit 7c820bb

File tree

3 files changed

+150
-3
lines changed

3 files changed

+150
-3
lines changed

src/Fieldtypes/Entries.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Statamic\Contracts\Entries\Entry as EntryContract;
88
use Statamic\CP\Column;
99
use Statamic\CP\Columns;
10+
use Statamic\Exceptions\AuthorizationException;
1011
use Statamic\Exceptions\CollectionNotFoundException;
1112
use Statamic\Facades\Blink;
1213
use Statamic\Facades\Collection;
@@ -132,12 +133,16 @@ protected function configFieldItems(): array
132133

133134
public function getIndexItems($request)
134135
{
136+
$configuredCollections = $this->getConfiguredCollections();
137+
$requestedCollections = $this->getRequestedCollections($request, $configuredCollections);
138+
$this->authorizeCollectionAccess($requestedCollections);
139+
135140
$query = $this->getIndexQuery($request);
136141

137142
$filters = $request->filters;
138143

139144
if (! isset($filters['collection'])) {
140-
$query->whereIn('collection', $this->getConfiguredCollections());
145+
$query->whereIn('collection', $configuredCollections);
141146
}
142147

143148
if ($blueprints = $this->config('blueprints')) {
@@ -157,6 +162,30 @@ public function getIndexItems($request)
157162
return $paginate ? $results->setCollection($items) : $items;
158163
}
159164

165+
private function getRequestedCollections($request, $configuredCollections)
166+
{
167+
$filteredCollections = collect($request->input('filters.collection.collections', []))
168+
->filter()
169+
->values()
170+
->all();
171+
172+
return empty($filteredCollections) ? $configuredCollections : $filteredCollections;
173+
}
174+
175+
private function authorizeCollectionAccess($collections)
176+
{
177+
$user = User::current();
178+
179+
collect($collections)->each(function ($collectionHandle) use ($user) {
180+
$collection = Collection::findByHandle($collectionHandle);
181+
182+
throw_if(
183+
! $collection || ! $user->can('view', $collection),
184+
new AuthorizationException
185+
);
186+
});
187+
}
188+
160189
public function getResourceCollection($request, $items)
161190
{
162191
return (new EntriesFieldtypeEntries($items, $this))

src/Fieldtypes/Terms.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Statamic\Contracts\Entries\Entry;
88
use Statamic\Contracts\Taxonomies\Term as TermContract;
99
use Statamic\CP\Column;
10+
use Statamic\Exceptions\AuthorizationException;
1011
use Statamic\Exceptions\TaxonomyNotFoundException;
1112
use Statamic\Exceptions\TermsFieldtypeBothOptionsUsedException;
1213
use Statamic\Exceptions\TermsFieldtypeTaxonomyOptionUsed;
@@ -257,6 +258,10 @@ public function getIndexItems($request)
257258
return collect();
258259
}
259260

261+
$this->authorizeTaxonomyAccess(
262+
$this->getRequestedTaxonomies($request, $this->getConfiguredTaxonomies())
263+
);
264+
260265
$query = $this->getIndexQuery($request);
261266

262267
if ($sort = $this->getSortColumn($request)) {
@@ -266,6 +271,27 @@ public function getIndexItems($request)
266271
return $request->boolean('paginate', true) ? $query->paginate() : $query->get();
267272
}
268273

274+
private function getRequestedTaxonomies($request, $configuredTaxonomies)
275+
{
276+
$requestedTaxonomies = collect($request->taxonomies)->filter()->values()->all();
277+
278+
return empty($requestedTaxonomies) ? $configuredTaxonomies : $requestedTaxonomies;
279+
}
280+
281+
private function authorizeTaxonomyAccess($taxonomies)
282+
{
283+
$user = User::current();
284+
285+
collect($taxonomies)->each(function ($taxonomyHandle) use ($user) {
286+
$taxonomy = Taxonomy::findByHandle($taxonomyHandle);
287+
288+
throw_if(
289+
! $taxonomy || ! $user->can('view', $taxonomy),
290+
new AuthorizationException
291+
);
292+
});
293+
}
294+
269295
public function getResourceCollection($request, $items)
270296
{
271297
return (new TermsResource($items, $this))

tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use PHPUnit\Framework\Attributes\Test;
66
use Statamic\Facades\Collection;
77
use Statamic\Facades\Entry;
8+
use Statamic\Facades\Taxonomy;
9+
use Statamic\Facades\Term;
810
use Statamic\Facades\User;
911
use Statamic\Query\Scopes\Scope;
1012
use Tests\FakesRoles;
@@ -35,7 +37,7 @@ public function it_filters_entries_by_query_scopes()
3537
Entry::make()->collection('test')->slug('cherry')->data(['title' => 'Cherry'])->save();
3638
Entry::make()->collection('test')->slug('banana')->data(['title' => 'Banana'])->save();
3739

38-
$this->setTestRoles(['test' => ['access cp']]);
40+
$this->setTestRoles(['test' => ['access cp', 'view test entries']]);
3941
$user = User::make()->assignRole('test')->save();
4042

4143
$config = base64_encode(json_encode([
@@ -46,7 +48,7 @@ public function it_filters_entries_by_query_scopes()
4648

4749
$response = $this
4850
->actingAs($user)
49-
->get("/cp/fieldtypes/relationship?config={$config}&collections[0]=test")
51+
->get("/cp/fieldtypes/relationship?config={$config}")
5052
->assertOk();
5153

5254
$titles = collect($response->json('data'))->pluck('title')->all();
@@ -57,6 +59,96 @@ public function it_filters_entries_by_query_scopes()
5759
$this->assertNotContains('Apple', $titles);
5860
$this->assertNotContains('Banana', $titles);
5961
}
62+
63+
#[Test]
64+
public function it_denies_access_to_entries_when_theres_a_collection_the_user_cannot_view()
65+
{
66+
Collection::make('secret')->save();
67+
Entry::make()->collection('secret')->slug('secret-one')->data(['title' => 'Secret One'])->save();
68+
69+
$this->setTestRoles(['test' => ['access cp']]);
70+
$user = User::make()->assignRole('test')->save();
71+
72+
$config = base64_encode(json_encode([
73+
'type' => 'entries',
74+
'collections' => ['secret'],
75+
]));
76+
77+
$this
78+
->actingAs($user)
79+
->getJson("/cp/fieldtypes/relationship?config={$config}")
80+
->assertForbidden();
81+
}
82+
83+
#[Test]
84+
public function it_forbids_access_to_entries_when_filters_target_a_collection_the_user_cannot_view()
85+
{
86+
Collection::make('secret')->save();
87+
Entry::make()->collection('test')->slug('apple')->data(['title' => 'Apple'])->save();
88+
Entry::make()->collection('secret')->slug('secret-one')->data(['title' => 'Secret One'])->save();
89+
90+
$this->setTestRoles([
91+
'test' => ['access cp', 'view test entries'],
92+
]);
93+
$user = User::make()->assignRole('test')->save();
94+
95+
$config = base64_encode(json_encode([
96+
'type' => 'entries',
97+
'collections' => ['test'],
98+
]));
99+
$filters = base64_encode(json_encode([
100+
'collection' => ['collections' => ['secret']],
101+
]));
102+
103+
$this
104+
->actingAs($user)
105+
->getJson("/cp/fieldtypes/relationship?config={$config}&filters={$filters}")
106+
->assertForbidden();
107+
}
108+
109+
#[Test]
110+
public function it_forbids_access_to_terms_when_config_contains_a_taxonomy_the_user_cannot_view()
111+
{
112+
Taxonomy::make('secret')->save();
113+
Term::make('internal')->taxonomy('secret')->data([])->save();
114+
115+
$this->setTestRoles(['test' => ['access cp']]);
116+
$user = User::make()->assignRole('test')->save();
117+
118+
$config = base64_encode(json_encode([
119+
'type' => 'terms',
120+
'taxonomies' => ['secret'],
121+
]));
122+
123+
$this
124+
->actingAs($user)
125+
->getJson("/cp/fieldtypes/relationship?config={$config}&taxonomies[0]=secret")
126+
->assertForbidden();
127+
}
128+
129+
#[Test]
130+
public function it_forbids_access_to_terms_when_requested_taxonomy_is_forbidden()
131+
{
132+
Taxonomy::make('topics')->save();
133+
Taxonomy::make('secret')->save();
134+
Term::make('public')->taxonomy('topics')->data([])->save();
135+
Term::make('internal')->taxonomy('secret')->data([])->save();
136+
137+
$this->setTestRoles([
138+
'test' => ['access cp', 'view topics terms'],
139+
]);
140+
$user = User::make()->assignRole('test')->save();
141+
142+
$config = base64_encode(json_encode([
143+
'type' => 'terms',
144+
'taxonomies' => ['topics'],
145+
]));
146+
147+
$this
148+
->actingAs($user)
149+
->getJson("/cp/fieldtypes/relationship?config={$config}&taxonomies[0]=secret")
150+
->assertForbidden();
151+
}
60152
}
61153

62154
class StartsWithC extends Scope

0 commit comments

Comments
 (0)