From 4119eec52322b5b5358166ab3367b723d0a15775 Mon Sep 17 00:00:00 2001
From: Ross Bale <ross.bale@computerminds.co.uk>
Date: Fri, 10 Jan 2025 10:37:55 +0000
Subject: [PATCH 1/7] Issue #3498739 - Allow returning a 403 response when
 blocking

---
 config/install/restrict_ip.settings.yml           |  3 +++
 config/schema/restrict_ip.schema.yml              |  3 +++
 restrict_ip.services.yml                          |  1 +
 src/EventSubscriber/RestrictIpEventSubscriber.php | 14 ++++++++++++++
 src/Form/ConfigForm.php                           | 13 +++++++++++++
 5 files changed, 34 insertions(+)

diff --git a/config/install/restrict_ip.settings.yml b/config/install/restrict_ip.settings.yml
index 71b69c1..0b8915e 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 8489931..f36df7e 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.services.yml b/restrict_ip.services.yml
index a027553..8d95d7c 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 6bc282a..202343f 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'),
     );
   }
 
@@ -77,6 +83,11 @@ class RestrictIpEventSubscriber implements EventSubscriberInterface, ContainerIn
    * Check if response needs to be restricted.
    */
   public function checkIpRestricted(RequestEvent $event): void {
+    // If we are on the system 403 route, bypass the inner logic.
+    if ($this->routeMatch->getRouteName() === 'system.403') {
+      return;
+    }
+
     unset($_SESSION['restrict_ip']);
 
     $this->restrictIpService->testForBlock();
@@ -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 5dda5e6..2135893 100644
--- a/src/Form/ConfigForm.php
+++ b/src/Form/ConfigForm.php
@@ -180,6 +180,18 @@ 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' => [
+        'visible' => [
+          '#edit-allow-role-bypass' => ['checked' => FALSE],
+        ]
+      ]
+    ];
+
     $form['white_black_list'] = [
       '#type' => 'radios',
       '#options' => [
@@ -394,6 +406,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)
-- 
GitLab


From 0bc094538e6b4b3105d4a8f8fa649d1ac08e8977 Mon Sep 17 00:00:00 2001
From: Ross Bale <ross.bale@computerminds.co.uk>
Date: Fri, 17 Jan 2025 14:28:39 +0000
Subject: [PATCH 2/7] #3498739 - Add test to check the new return_403 option
 works as expected.

---
 tests/src/Functional/RestrictIpAccessTest.php | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/tests/src/Functional/RestrictIpAccessTest.php b/tests/src/Functional/RestrictIpAccessTest.php
index 971877e..de27659 100644
--- a/tests/src/Functional/RestrictIpAccessTest.php
+++ b/tests/src/Functional/RestrictIpAccessTest.php
@@ -171,6 +171,28 @@ class RestrictIpAccessTest extends RestrictIpBrowserTestBase {
     $this->assertElementExists('#edit-name');
   }
 
+  /**
+   * Tests if access denied response is returned when the 'return_403'
+   * option is 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.
    *
-- 
GitLab


From cd3751ed0284f835cff92aa0071640d942e388e2 Mon Sep 17 00:00:00 2001
From: Ross Bale <ross.bale@computerminds.co.uk>
Date: Fri, 17 Jan 2025 14:31:09 +0000
Subject: [PATCH 3/7] #3498739 - Tweak the config form visibility of return_403
 option to match the logic in ::checkIpRestricted(). - The return_403 option
 doesn't kick in if allow_role_bypass is enabled AND the bypass_action is
 redirect_login_page, the config form visibility should match this.

---
 src/Form/ConfigForm.php | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/Form/ConfigForm.php b/src/Form/ConfigForm.php
index 2135893..c08ad67 100644
--- a/src/Form/ConfigForm.php
+++ b/src/Form/ConfigForm.php
@@ -186,8 +186,12 @@ class ConfigForm extends ConfigFormBase {
       '#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' => [
-        'visible' => [
-          '#edit-allow-role-bypass' => ['checked' => FALSE],
+        'invisible' => [
+          [
+            '#edit-allow-role-bypass' => ['checked' => TRUE],
+            'and',
+            'input[name="bypass_action"]' => ['value' => 'redirect_login_page'],
+          ],
         ]
       ]
     ];
-- 
GitLab


From 7671b56de6a42b126aa7fc2f09e40c9c7b961f19 Mon Sep 17 00:00:00 2001
From: Ross Bale <ross.bale@computerminds.co.uk>
Date: Fri, 17 Jan 2025 14:36:17 +0000
Subject: [PATCH 4/7] #3498739 - Set the new return_403 value in config to
 false.

---
 restrict_ip.install | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/restrict_ip.install b/restrict_ip.install
index c9e308f..084899c 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();
+}
-- 
GitLab


From 0a28409f85fe5f4e3d600b52496e640cbfc673ef Mon Sep 17 00:00:00 2001
From: Ross Bale <ross.bale@computerminds.co.uk>
Date: Fri, 17 Jan 2025 14:49:27 +0000
Subject: [PATCH 5/7] #3498739 - Fix PHPCS violations with the newly added
 logic.

---
 src/EventSubscriber/RestrictIpEventSubscriber.php | 4 ++--
 src/Form/ConfigForm.php                           | 4 ++--
 tests/src/Functional/RestrictIpAccessTest.php     | 3 +--
 3 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/EventSubscriber/RestrictIpEventSubscriber.php b/src/EventSubscriber/RestrictIpEventSubscriber.php
index 202343f..a87d9b9 100644
--- a/src/EventSubscriber/RestrictIpEventSubscriber.php
+++ b/src/EventSubscriber/RestrictIpEventSubscriber.php
@@ -40,8 +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.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
+   *   The route match handler.
    */
   public function __construct(
     protected RestrictIpServiceInterface $restrictIpService,
diff --git a/src/Form/ConfigForm.php b/src/Form/ConfigForm.php
index c08ad67..667b8c7 100644
--- a/src/Form/ConfigForm.php
+++ b/src/Form/ConfigForm.php
@@ -192,8 +192,8 @@ class ConfigForm extends ConfigFormBase {
             'and',
             'input[name="bypass_action"]' => ['value' => 'redirect_login_page'],
           ],
-        ]
-      ]
+        ],
+      ],
     ];
 
     $form['white_black_list'] = [
diff --git a/tests/src/Functional/RestrictIpAccessTest.php b/tests/src/Functional/RestrictIpAccessTest.php
index de27659..85625bf 100644
--- a/tests/src/Functional/RestrictIpAccessTest.php
+++ b/tests/src/Functional/RestrictIpAccessTest.php
@@ -172,8 +172,7 @@ class RestrictIpAccessTest extends RestrictIpBrowserTestBase {
   }
 
   /**
-   * Tests if access denied response is returned when the 'return_403'
-   * option is enabled.
+   * Tests access denied response is returned when 'return_403' option enabled.
    */
   public function testAccessDeniedResponseWhenAccessDeniedOptionEnabled(): void {
     $adminUser = $this->drupalCreateUser([
-- 
GitLab


From 4f0fdf5156f363d7fc8b575429aa2069562ec844 Mon Sep 17 00:00:00 2001
From: Ross Bale <ross.bale@computerminds.co.uk>
Date: Fri, 17 Jan 2025 15:18:54 +0000
Subject: [PATCH 6/7] #3498739 - Fix PHPCS violations with the newly added
 logic.

---
 src/Form/ConfigForm.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Form/ConfigForm.php b/src/Form/ConfigForm.php
index 667b8c7..7b54633 100644
--- a/src/Form/ConfigForm.php
+++ b/src/Form/ConfigForm.php
@@ -189,7 +189,6 @@ class ConfigForm extends ConfigFormBase {
         'invisible' => [
           [
             '#edit-allow-role-bypass' => ['checked' => TRUE],
-            'and',
             'input[name="bypass_action"]' => ['value' => 'redirect_login_page'],
           ],
         ],
-- 
GitLab


From 8224dcb934e0d752ddd7b5fe1dbc6f1f4adc8fa3 Mon Sep 17 00:00:00 2001
From: Ross Bale <ross.bale@computerminds.co.uk>
Date: Tue, 21 Jan 2025 15:12:33 +0000
Subject: [PATCH 7/7] #3498739 - Ensure $_SESSION['restrict_ip'] always gets
 unset even on a 403 route.

---
 src/EventSubscriber/RestrictIpEventSubscriber.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/EventSubscriber/RestrictIpEventSubscriber.php b/src/EventSubscriber/RestrictIpEventSubscriber.php
index a87d9b9..f080b9c 100644
--- a/src/EventSubscriber/RestrictIpEventSubscriber.php
+++ b/src/EventSubscriber/RestrictIpEventSubscriber.php
@@ -83,13 +83,13 @@ class RestrictIpEventSubscriber implements EventSubscriberInterface, ContainerIn
    * Check if response needs to be restricted.
    */
   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;
     }
 
-    unset($_SESSION['restrict_ip']);
-
     $this->restrictIpService->testForBlock();
     $config = $this->configFactory->get('restrict_ip.settings');
     if ($this->restrictIpService->userIsBlocked()) {
-- 
GitLab