diff --git a/config/install/restrict_ip.settings.yml b/config/install/restrict_ip.settings.yml index 71b69c1068b126d9db29a608a6f00754c67ebed5..0b8915ec4bf9211cf2d939b695cebe2d05353697 100644 --- a/config/install/restrict_ip.settings.yml +++ b/config/install/restrict_ip.settings.yml @@ -18,6 +18,9 @@ allow_role_bypass: false # A key indicating what action should be performed when bypasses are allowed by role, but users are not logged in bypass_action: 'provide_link_login_page' +# A boolean indicating whether to return a 403 access denied response when blocking. +return_403: false + # D7 variable - restrict_ip_white_black_list # An integer indicating how to check bypass restrictions. # 0 = check access on all paths diff --git a/config/schema/restrict_ip.schema.yml b/config/schema/restrict_ip.schema.yml index 84899315f5680e25c21eb18a11498db82ae166c8..f36df7e13a70abae2f97657918a8cfd27afa8b09 100644 --- a/config/schema/restrict_ip.schema.yml +++ b/config/schema/restrict_ip.schema.yml @@ -23,6 +23,9 @@ restrict_ip.settings: Choice: - provide_link_login_page - redirect_login_page + return_403: + type: boolean + label: 'Return a 403 access denied response when blocking' white_black_list: type: integer label: 'Whether to use a path whitelist, blacklist, or check all pages' diff --git a/restrict_ip.install b/restrict_ip.install index c9e308fbce399accdc505a13e76017f7b0e5273f..084899c1d4c97bb04797c25e2303ab28f42aa7a2 100644 --- a/restrict_ip.install +++ b/restrict_ip.install @@ -142,3 +142,12 @@ function restrict_ip_update_500001(): void { function restrict_ip_update_500002(): void { \Drupal::configFactory()->getEditable('restrict_ip.settings')->save(); } + +/** + * Ensure the new return_403 option is present in config (disabled by default). + */ +function restrict_ip_update_500003(): void { + \Drupal::configFactory()->getEditable('restrict_ip.settings') + ->set('return_403', FALSE) + ->save(); +} diff --git a/restrict_ip.services.yml b/restrict_ip.services.yml index a027553129520723ffffd8f799a042b8934198e0..8d95d7c334e453fab2592ffd227524f782985aff 100644 --- a/restrict_ip.services.yml +++ b/restrict_ip.services.yml @@ -31,6 +31,7 @@ services: - '@module_handler' - '@url_generator' - '@messenger' + - '@current_route_match' tags: - {name: event_subscriber} diff --git a/src/EventSubscriber/RestrictIpEventSubscriber.php b/src/EventSubscriber/RestrictIpEventSubscriber.php index 6bc282a417023ee5f6d6cc8513f60afc84106466..f080b9c2f6ee82ecc1a5976db3d20ee48b9edced 100644 --- a/src/EventSubscriber/RestrictIpEventSubscriber.php +++ b/src/EventSubscriber/RestrictIpEventSubscriber.php @@ -7,6 +7,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Messenger\Messenger; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; @@ -15,6 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\KernelEvents; /** @@ -38,6 +40,8 @@ class RestrictIpEventSubscriber implements EventSubscriberInterface, ContainerIn * The Url Generator service. * @param \Drupal\Core\Messenger\Messenger $messenger * The messenger service object. + * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch + * The route match handler. */ public function __construct( protected RestrictIpServiceInterface $restrictIpService, @@ -46,6 +50,7 @@ class RestrictIpEventSubscriber implements EventSubscriberInterface, ContainerIn protected ModuleHandlerInterface $moduleHandler, protected UrlGeneratorInterface $urlGenerator, protected Messenger $messenger, + protected RouteMatchInterface $routeMatch, ) { } @@ -60,6 +65,7 @@ class RestrictIpEventSubscriber implements EventSubscriberInterface, ContainerIn $container->get('module_handler'), $container->get('url_generator'), $container->get('messenger'), + $container->get('current_route_match'), ); } @@ -79,6 +85,11 @@ class RestrictIpEventSubscriber implements EventSubscriberInterface, ContainerIn public function checkIpRestricted(RequestEvent $event): void { unset($_SESSION['restrict_ip']); + // If we are on the system 403 route, bypass the inner logic. + if ($this->routeMatch->getRouteName() === 'system.403') { + return; + } + $this->restrictIpService->testForBlock(); $config = $this->configFactory->get('restrict_ip.settings'); if ($this->restrictIpService->userIsBlocked()) { @@ -95,6 +106,9 @@ class RestrictIpEventSubscriber implements EventSubscriberInterface, ContainerIn $url = Url::fromRoute('user.login'); $event->setResponse(new RedirectResponse($url->toString())); } + elseif ($config->get('return_403')) { + throw new AccessDeniedHttpException(); + } elseif (in_array($config->get('white_black_list'), [0, 1])) { $url = Url::fromRoute('restrict_ip.access_denied_page'); $event->setResponse(new RedirectResponse($url->toString())); diff --git a/src/Form/ConfigForm.php b/src/Form/ConfigForm.php index 5dda5e66847a07b372194214b0a3717fba2731b9..7b54633285ab91d07ab1fa5a94c61dda5cf6651e 100644 --- a/src/Form/ConfigForm.php +++ b/src/Form/ConfigForm.php @@ -180,6 +180,21 @@ class ConfigForm extends ConfigFormBase { ], ]; + $form['return_403'] = [ + '#title' => $this->t('Return a 403 access denied response'), + '#type' => 'checkbox', + '#default_value' => $config->get('return_403'), + '#description' => $this->t('When this box is checked, a 403 access denied response will be returned instead of the access denied page or front page.'), + '#states' => [ + 'invisible' => [ + [ + '#edit-allow-role-bypass' => ['checked' => TRUE], + 'input[name="bypass_action"]' => ['value' => 'redirect_login_page'], + ], + ], + ], + ]; + $form['white_black_list'] = [ '#type' => 'radios', '#options' => [ @@ -394,6 +409,7 @@ class ConfigForm extends ConfigFormBase { ->set('dblog', (bool) $form_state->getValue('dblog')) ->set('allow_role_bypass', (bool) $form_state->getValue('allow_role_bypass')) ->set('bypass_action', (string) $form_state->getValue('bypass_action')) + ->set('return_403', (bool) $form_state->getValue('return_403')) ->set('white_black_list', (int) $form_state->getValue('white_black_list')) ->set('country_white_black_list', (int) $form_state->getValue('country_white_black_list')) ->set('country_list', $country_list) diff --git a/tests/src/Functional/RestrictIpAccessTest.php b/tests/src/Functional/RestrictIpAccessTest.php index 971877e3d5bae67c31930209e25a8f1816e6841a..85625bf317acb1fda23a65182cf01304adc9cb18 100644 --- a/tests/src/Functional/RestrictIpAccessTest.php +++ b/tests/src/Functional/RestrictIpAccessTest.php @@ -171,6 +171,27 @@ class RestrictIpAccessTest extends RestrictIpBrowserTestBase { $this->assertElementExists('#edit-name'); } + /** + * Tests access denied response is returned when 'return_403' option enabled. + */ + public function testAccessDeniedResponseWhenAccessDeniedOptionEnabled(): void { + $adminUser = $this->drupalCreateUser([ + 'administer restricted ip addresses', + 'access administration pages', + 'administer modules', + ]); + + $this->drupalLogin($adminUser); + $this->drupalGet('admin/config/people/restrict_ip'); + $this->assertStatusCodeEquals(200); + + $this->checkCheckbox('#edit-enable'); + $this->checkCheckbox('#edit-return-403'); + $this->click('#edit-submit'); + + $this->assertStatusCodeEquals(403); + } + /** * Tests whitelisting paths. *