diff --git a/config/schema/rabbit_hole.schema.yml b/config/schema/rabbit_hole.schema.yml index 93f9bb720d6261cd89b893117ddf4dfd004367a3..fb2785fdc99c3374cd7eba4f981a07869cd942a0 100644 --- a/config/schema/rabbit_hole.schema.yml +++ b/config/schema/rabbit_hole.schema.yml @@ -31,6 +31,9 @@ rabbit_hole.behavior_settings.*: action: type: string label: 'Action' + no_bypass: + type: boolean + label: 'No bypass' configuration: type: rabbit_hole.behavior.[%parent.action] diff --git a/rabbit_hole.install b/rabbit_hole.install index cf1ec801408700a66bb5aca6e16fcd88e196799d..41bad45a58f3cfa0a8df28d75a114e18206266da 100644 --- a/rabbit_hole.install +++ b/rabbit_hole.install @@ -9,6 +9,7 @@ use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Site\Settings; use Drupal\field\Entity\FieldConfig; use Drupal\rabbit_hole\BehaviorSettingsManager; +use Drupal\rabbit_hole\Entity\BehaviorSettings; /** * Install redirect_fallback_action field. @@ -344,3 +345,21 @@ function rabbit_hole_update_8109() { ->getEditable('rabbit_hole.behavior_settings.default') ->delete(); } + +/** + * Add "no_bypass" settings to existing configuration. + */ +function rabbit_hole_update_8110() { + $config_factory = \Drupal::configFactory(); + $configs = $config_factory->listAll('rabbit_hole.behavior_settings'); + + foreach ($configs as $config) { + $config = $config_factory->getEditable($config); + // Skip configs that already have "no_bypass" values. + if ($config->get('no_bypass') !== NULL) { + continue; + } + $config->set('no_bypass', FALSE); + $config->save(TRUE); + } +} diff --git a/src/BehaviorInvoker.php b/src/BehaviorInvoker.php index e44317ba22d880ef31e34b082706cd056180b2d5..d86492eaf6bab1f99fbe8fa6725730b5d972208a 100644 --- a/src/BehaviorInvoker.php +++ b/src/BehaviorInvoker.php @@ -154,8 +154,12 @@ class BehaviorInvoker implements BehaviorInvokerInterface { // Adding a note that prefixed values are deprecated. $values['deprecation_note'] = 'Prefixed values (rh_action, rh_redirect, etc.) are deprecated and will be removed in next versions. Use properties without "rh_" prefix.'; - $permission = 'rabbit hole bypass ' . $entity->getEntityTypeId(); - $values['bypass_access'] = $this->currentUser->hasPermission($permission); + // Perform the access check if bypass is not disabled. + $values['bypass_access'] = FALSE; + if (empty($values['no_bypass'])) { + $permission = 'rabbit hole bypass ' . $entity->getEntityTypeId(); + $values['bypass_access'] = $this->currentUser->hasPermission($permission); + } // Allow altering Rabbit Hole values. $this->moduleHandler->alter('rabbit_hole_values', $values, $entity); diff --git a/src/BehaviorSettingsInterface.php b/src/BehaviorSettingsInterface.php index 679e396488c1c0a9dacad43d2b2e7e24a218afa9..ff95c4fa27085d6b9ea273e15a85439324649d0f 100644 --- a/src/BehaviorSettingsInterface.php +++ b/src/BehaviorSettingsInterface.php @@ -25,4 +25,20 @@ interface BehaviorSettingsInterface extends ConfigEntityInterface { */ public function getAction(); + /** + * Set whether to ignore bypass permissions. + * + * @param bool $no_bypass + * TRUE - ignore, FALSE - do not ignore. + */ + public function setNoBypass(bool $no_bypass); + + /** + * Get whether to ignore bypass permissions. + * + * @return bool + * TRUE - ignore, FALSE - do not ignore. + */ + public function getNoBypass(): bool; + } diff --git a/src/BehaviorSettingsManager.php b/src/BehaviorSettingsManager.php index 9957df3b88ceedd2b89cf13f60d4d0feb6c00c9c..1550d5e23bde9af36a7c031c8076ae62870789f0 100644 --- a/src/BehaviorSettingsManager.php +++ b/src/BehaviorSettingsManager.php @@ -122,6 +122,7 @@ class BehaviorSettingsManager implements BehaviorSettingsManagerInterface { $default = $this->configFactory->get('rabbit_hole.behavior_settings.default'); return !$default->isNew() ? $default->get() : [ 'action' => 'display_page', + 'no_bypass' => FALSE, 'configuration' => [], ]; } @@ -130,8 +131,10 @@ class BehaviorSettingsManager implements BehaviorSettingsManagerInterface { * {@inheritdoc} */ public function getEntityBehaviorSettings(ContentEntityInterface $entity): array { - $values = []; $config_data = $this->getBehaviorSettings($entity->getEntityTypeId(), $entity->bundle()); + $values = [ + 'no_bypass' => $config_data['no_bypass'], + ]; // We trigger the default bundle action under the following circumstances: $trigger_default_bundle_action = diff --git a/src/Entity/BehaviorSettings.php b/src/Entity/BehaviorSettings.php index 41701064a215dcc898884c1e49687b828c37a0fd..63b3d64a668aa6be1c8e0583676a40f58b5da28a 100644 --- a/src/Entity/BehaviorSettings.php +++ b/src/Entity/BehaviorSettings.php @@ -25,6 +25,7 @@ use Drupal\rabbit_hole\BehaviorSettingsInterface; * "entity_id", * "uuid", * "action", + * "no_bypass", * "configuration" * }, * links = {} @@ -60,6 +61,13 @@ class BehaviorSettings extends ConfigEntityBase implements BehaviorSettingsInter */ protected $entity_id; + /** + * The bypass action. + * + * @var bool + */ + protected $no_bypass; + /** * The action-specific configuration. * @@ -81,6 +89,20 @@ class BehaviorSettings extends ConfigEntityBase implements BehaviorSettingsInter return $this->action; } + /** + * {@inheritdoc} + */ + public function setNoBypass(bool $no_bypass) { + $this->no_bypass = $no_bypass; + } + + /** + * {@inheritdoc} + */ + public function getNoBypass(): bool { + return $this->no_bypass; + } + /** * {@inheritdoc} */ diff --git a/src/FormManglerService.php b/src/FormManglerService.php index bb5eb8251275aeca9c48da73a8d2e47122e837af..39a82accc3df21323e7e050ceda174fd7604db45 100644 --- a/src/FormManglerService.php +++ b/src/FormManglerService.php @@ -124,10 +124,17 @@ class FormManglerService { '#type' => 'checkbox', '#title' => $this->t('Allow these settings to be overridden for individual entities'), '#default_value' => $this->entityHelper->hasRabbitHoleField($entity_type->id(), $bundle_name), - '#description' => $this->t('If this is checked, users with the %permission permission will be able to override these settings for individual entities.', [ + '#description' => $this->t('If checked, users with the %permission permission will be able to override these settings for individual entities.', [ '%permission' => $this->t('Administer Rabbit Hole settings for @entity_type', ['@entity_type' => $entity_type->getLabel()]), ]), ]; + + $form['no_bypass'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Disable permissions-based bypassing'), + '#default_value' => $settings['no_bypass'], + '#description' => $this->t("If checked, users won't be able to bypass configured Rabbit Hole behavior. It will be applied to Administrators and other users with bypass permissions."), + ]; } /** @@ -162,6 +169,7 @@ class FormManglerService { $settings = [ 'action' => $action, + 'no_bypass' => $form_values['no_bypass'], ]; // Get action settings if it exists in the form. diff --git a/tests/src/Kernel/BehaviorInvokerTest.php b/tests/src/Kernel/BehaviorInvokerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a13178392e4da681e518310f2b93f5e709368191 --- /dev/null +++ b/tests/src/Kernel/BehaviorInvokerTest.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\Tests\rabbit_hole\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\rabbit_hole\Plugin\RabbitHoleBehaviorPlugin\PageNotFound; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Test cases for BehaviorInvoker. + * + * @coversDefaultClass \Drupal\rabbit_hole\BehaviorInvoker + * @group rabbit_hole + */ +class BehaviorInvokerTest extends KernelTestBase { + + use ContentTypeCreationTrait; + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'system', + 'filter', + 'text', + 'field', + 'user', + 'node', + 'rabbit_hole', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig(['filter', 'node', 'system', 'rabbit_hole']); + + \Drupal::service('rabbit_hole.behavior_settings_manager')->enableEntityType('node'); + + $this->createContentType(['type' => 'article']); + $this->createContentType(['type' => 'page']); + + \Drupal::service('rabbit_hole.behavior_settings_manager')->saveBehaviorSettings([ + 'action' => 'page_not_found', + 'no_bypass' => FALSE, + ], 'node_type', 'article'); + + \Drupal::service('rabbit_hole.behavior_settings_manager')->saveBehaviorSettings([ + 'action' => 'page_not_found', + 'no_bypass' => TRUE, + ], 'node_type', 'page'); + } + + /** + * @covers ::getBehaviorPlugin() + */ + public function testGetBehaviorPlugin() { + $node1 = Node::create(['title' => '#freeAzov', 'type' => 'article']); + $node1->save(); + $node2 = Node::create(['title' => 'Support Ukraine', 'type' => 'page']); + $node2->save(); + + $behavior_invoker = \Drupal::service('rabbit_hole.behavior_invoker'); + + $this->setUpCurrentUser([], [], TRUE); + // "No bypass" for articles is disabled, so admin should see the page. + // In other words, the plugin should be not available. + $this->assertNull($behavior_invoker->getBehaviorPlugin($node1)); + // For pages, "no bypass" is enabled, so action plugin is expected. + $this->assertInstanceOf(PageNotFound::class, $behavior_invoker->getBehaviorPlugin($node2)); + + // Verify that regular user cannot access article page. + $this->setUpCurrentUser(); + $this->assertInstanceOf(PageNotFound::class, $behavior_invoker->getBehaviorPlugin($node1)); + } + +}