Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions web/modules/custom/do_content_api/do_content_api.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ services:
class: Drupal\do_content_api\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
Drupal\do_content_api\Hook\EntityCreateAccessHook:
arguments:
- '@request_stack'
Drupal\do_content_api\Hook\ModerationPolicyHook:
arguments:
- '@current_user'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* Grants paragraph create access to authorised content-authoring clients.
*/
final class EntityCreateAccessHook {

public function __construct(
protected RequestStack $requestStack,
) {}

/**
* Paragraph bundles that may be created through the authoring API.
*/
Expand Down Expand Up @@ -45,22 +50,29 @@ public function entityCreateAccess(AccountInterface $account, array $context, ?s
return AccessResult::neutral();
}

// The paragraphs access handler already grants create access on HTML
// requests, and a forbidden result here would override it for every role
// that bypasses permission checks and so matches the gate below.
if ($this->requestStack->getCurrentRequest()?->getRequestFormat() === 'html') {
return AccessResult::neutral()->addCacheContexts(['request_format']);
}

// Editorial users keep the stock paragraphs access behaviour.
if (!$account->hasPermission('use content authoring api')) {
return AccessResult::neutral()->cachePerPermissions();
return AccessResult::neutral()->cachePerPermissions()->addCacheContexts(['request_format']);
}

// A bundle-less capability check gets no opinion.
if ($entity_bundle === NULL) {
return AccessResult::neutral()->cachePerPermissions();
return AccessResult::neutral()->cachePerPermissions()->addCacheContexts(['request_format']);
}

// The paragraphs access handler returns neutral for every non-HTML request
// format. Restore create access for the allow-listed bundles and explicitly
// deny the rest so no other handler can widen the authoring surface.
return in_array($entity_bundle, self::ALLOWED_PARAGRAPH_BUNDLES, TRUE)
? AccessResult::allowed()->cachePerPermissions()
: AccessResult::forbidden()->cachePerPermissions();
? AccessResult::allowed()->cachePerPermissions()->addCacheContexts(['request_format'])
: AccessResult::forbidden()->cachePerPermissions()->addCacheContexts(['request_format']);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* Tests for EntityCreateAccessHook.
Expand All @@ -22,11 +24,19 @@ class EntityCreateAccessHookTest extends UnitTestCase {
* Tests paragraph create-access decisions.
*/
#[DataProvider('dataProviderEntityCreateAccess')]
public function testEntityCreateAccess(array $context, bool $has_permission, ?string $bundle, string $expected_state): void {
public function testEntityCreateAccess(array $context, bool $has_permission, ?string $bundle, ?string $request_format, string $expected_state): void {
// Prepare.
$account = $this->createMock(AccountInterface::class);
$account->method('hasPermission')->with('use content authoring api')->willReturn($has_permission);
$hook = new EntityCreateAccessHook();
$request_stack = new RequestStack();

if ($request_format !== NULL) {
$request = Request::create('/');
$request->setRequestFormat($request_format);
$request_stack->push($request);
}

$hook = new EntityCreateAccessHook($request_stack);

// Act.
$result = $hook->entityCreateAccess($account, $context, $bundle);
Expand All @@ -41,13 +51,16 @@ public function testEntityCreateAccess(array $context, bool $has_permission, ?st
* Data provider for testEntityCreateAccess().
*/
public static function dataProviderEntityCreateAccess(): \Iterator {
yield 'non-paragraph entity type is ignored' => [['entity_type_id' => 'node'], TRUE, 'civictheme_content', 'neutral'];
yield 'missing entity type id is ignored' => [[], TRUE, 'civictheme_content', 'neutral'];
yield 'permitted user, allowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_content', 'allowed'];
yield 'permitted user, nested allowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_accordion_panel', 'allowed'];
yield 'permitted user, disallowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_event_card_ref', 'forbidden'];
yield 'unpermitted user, allowed bundle' => [['entity_type_id' => 'paragraph'], FALSE, 'civictheme_content', 'neutral'];
yield 'permitted user, null bundle' => [['entity_type_id' => 'paragraph'], TRUE, NULL, 'neutral'];
yield 'non-paragraph entity type is ignored' => [['entity_type_id' => 'node'], TRUE, 'civictheme_content', 'api_json', 'neutral'];
yield 'missing entity type id is ignored' => [[], TRUE, 'civictheme_content', 'api_json', 'neutral'];
yield 'permitted user, allowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_content', 'api_json', 'allowed'];
yield 'permitted user, nested allowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_accordion_panel', 'api_json', 'allowed'];
yield 'permitted user, disallowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_event_card_ref', 'api_json', 'forbidden'];
yield 'unpermitted user, allowed bundle' => [['entity_type_id' => 'paragraph'], FALSE, 'civictheme_content', 'api_json', 'neutral'];
yield 'permitted user, null bundle' => [['entity_type_id' => 'paragraph'], TRUE, NULL, 'api_json', 'neutral'];
yield 'html request, disallowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_event_card_ref', 'html', 'neutral'];
yield 'html request, allowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_content', 'html', 'neutral'];
yield 'no request, disallowed bundle' => [['entity_type_id' => 'paragraph'], TRUE, 'civictheme_event_card_ref', NULL, 'forbidden'];
}

}
Loading