Skip to content
37 changes: 16 additions & 21 deletions src/Fieldtypes/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,23 @@ 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(fn ($collection) => $collection && $user->can('view', $collection))
->map->handle()
->all()
);
}

if ($blueprints = $this->config('blueprints')) {
Expand All @@ -162,28 +170,15 @@ 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)
private function authorizeCollectionAccess(array $collections): void
{
$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(fn ($collection) => $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)
Expand Down
46 changes: 22 additions & 24 deletions src/Fieldtypes/Terms.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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)
Expand All @@ -311,9 +300,13 @@ protected function getBlueprint($request)

protected function getFirstTaxonomyFromRequest($request)
{
return $request->taxonomies
? Facades\Taxonomy::findByHandle($request->taxonomies[0])
: 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)
Expand Down Expand Up @@ -434,10 +427,15 @@ protected function getColumns()
protected function getIndexQuery($request)
{
$query = Term::query();
$user = User::current();

if ($taxonomies = $request->taxonomies) {
$query->whereIn('taxonomy', $taxonomies);
}
$taxonomies = collect($this->getConfiguredTaxonomies())
->map(fn (string $taxonomyHandle) => Taxonomy::findByHandle($taxonomyHandle))
->filter(fn ($taxonomy) => $taxonomy && $user->can('view', $taxonomy))
->map->handle()
->all();

$query->whereIn('taxonomy', $taxonomies);

if ($search = $request->search) {
$query->where('title', 'like', '%'.$search.'%');
Expand Down
25 changes: 23 additions & 2 deletions src/Query/Scopes/Filters/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,6 +31,8 @@ public function fieldItems()

public function apply($query, $values)
{
$this->authorizeCollectionAccess($values['collections']);

$query->whereIn('collection', $values['collections']);
}

Expand All @@ -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) => $collection && $user->can('view', $collection))
->mapWithKeys(fn ($collection) => [$collection->handle() => $collection->title()]);
}

private function authorizeCollectionAccess(array $collections): void
{
$user = User::current();

collect($collections)->each(function (string $collectionHandle) use ($user) {
$collection = Facades\Collection::findByHandle($collectionHandle);

throw_if(
! $collection || ! $user->can('view', $collection),
new AuthorizationException
);
});
}
}
66 changes: 52 additions & 14 deletions tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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
Expand All @@ -81,7 +113,7 @@ 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('secret')->save();
Entry::make()->collection('test')->slug('apple')->data(['title' => 'Apple'])->save();
Expand All @@ -107,46 +139,52 @@ public function it_forbids_access_to_entries_when_filters_target_a_collection_th
}

#[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}")
->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}")
->assertForbidden();
}
}
Expand Down
Loading