From c704db94800243cbf1c4eb9a2a8c1e260e9eb75e Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Tue, 11 Apr 2023 13:07:01 +0100
Subject: [PATCH] Issue #3311563 by stefanos.petrakis, rpayanm, smustgrave,
 longwave, Wim Leers: Safeguarding against UnblockUser::execute()'s method
 unblocking the anonymous user

---
 core/modules/user/src/AccountForm.php         |  2 +-
 core/modules/user/src/Entity/User.php         |  5 +-
 .../user/src/UserAccessControlHandler.php     |  2 +-
 .../src/Kernel/UserAccountFormFieldsTest.php  | 23 ++++++++--
 .../UserAccountFormPasswordResetTest.php      | 11 +++--
 .../src/Kernel/UserAnonymousActivateTest.php  | 46 +++++++++++++++++++
 .../src/Unit/UserAccessControlHandlerTest.php | 16 +++----
 7 files changed, 84 insertions(+), 21 deletions(-)
 create mode 100644 core/modules/user/tests/src/Kernel/UserAnonymousActivateTest.php

diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php
index 1cbd8704c1c9..e51b0f9ea0d1 100644
--- a/core/modules/user/src/AccountForm.php
+++ b/core/modules/user/src/AccountForm.php
@@ -72,7 +72,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $language_interface = \Drupal::languageManager()->getCurrentLanguage();
 
     // Check for new account.
-    $register = $account->isAnonymous();
+    $register = $account->isNew();
 
     // For a new account, there are 2 sub-cases:
     // $self_register: A user creates their own, new, account
diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php
index 1df5d88dce65..9a653b191892 100644
--- a/core/modules/user/src/Entity/User.php
+++ b/core/modules/user/src/Entity/User.php
@@ -305,6 +305,9 @@ public function isBlocked() {
    * {@inheritdoc}
    */
   public function activate() {
+    if ($this->isAnonymous()) {
+      throw new \LogicException('The anonymous user account should remain blocked at all times.');
+    }
     $this->get('status')->value = 1;
     return $this;
   }
@@ -370,7 +373,7 @@ public function isAuthenticated() {
    * {@inheritdoc}
    */
   public function isAnonymous() {
-    return $this->id() == 0;
+    return $this->id() === 0 || $this->id() === '0';
   }
 
   /**
diff --git a/core/modules/user/src/UserAccessControlHandler.php b/core/modules/user/src/UserAccessControlHandler.php
index a231d5214de9..46b97cfa2e1e 100644
--- a/core/modules/user/src/UserAccessControlHandler.php
+++ b/core/modules/user/src/UserAccessControlHandler.php
@@ -101,7 +101,7 @@ protected function checkFieldAccess($operation, FieldDefinitionInterface $field_
       case 'name':
         // Allow view access to anyone with access to the entity.
         // The username field is editable during the registration process.
-        if ($operation == 'view' || ($items && $items->getEntity()->isAnonymous())) {
+        if ($operation == 'view' || ($items && $items->getEntity()->isNew())) {
           return AccessResult::allowed()->cachePerPermissions();
         }
         // Allow edit access for the own user name if the permission is
diff --git a/core/modules/user/tests/src/Kernel/UserAccountFormFieldsTest.php b/core/modules/user/tests/src/Kernel/UserAccountFormFieldsTest.php
index 1aabea79a8a5..0eac0422d089 100644
--- a/core/modules/user/tests/src/Kernel/UserAccountFormFieldsTest.php
+++ b/core/modules/user/tests/src/Kernel/UserAccountFormFieldsTest.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Form\FormState;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\user\Entity\User;
+use Drupal\user\UserInterface;
 
 /**
  * Verifies that the field order in user account forms is compatible with
@@ -20,6 +22,11 @@ class UserAccountFormFieldsTest extends KernelTestBase {
    */
   protected static $modules = ['system', 'user', 'field'];
 
+  /**
+   * @var \Drupal\user\UserInterface
+   */
+  protected UserInterface $user;
+
   /**
    * Tests the root user account form section in the "Configure site" form.
    */
@@ -72,6 +79,10 @@ public function testUserRegistrationForm() {
   public function testUserEditForm() {
     // Install default configuration; required for AccountFormController.
     $this->installConfig(['user']);
+    $this->installEntitySchema('user');
+
+    $this->user = User::create(['name' => 'test']);
+    $this->user->save();
 
     $form = $this->buildAccountForm('default');
 
@@ -127,13 +138,15 @@ protected function assertFieldOrder(array $elements): void {
   protected function buildAccountForm($operation) {
     // @see HtmlEntityFormController::getFormObject()
     $entity_type = 'user';
-    $fields = [];
     if ($operation != 'register') {
-      $fields['uid'] = 2;
+      // Use an existing user.
+      $entity = $this->user;
+    }
+    else {
+      $entity = $this->container->get('entity_type.manager')
+        ->getStorage($entity_type)
+        ->create();
     }
-    $entity = $this->container->get('entity_type.manager')
-      ->getStorage($entity_type)
-      ->create($fields);
 
     // @see EntityFormBuilder::getForm()
     return $this->container->get('entity.form_builder')->getForm($entity, $operation);
diff --git a/core/modules/user/tests/src/Kernel/UserAccountFormPasswordResetTest.php b/core/modules/user/tests/src/Kernel/UserAccountFormPasswordResetTest.php
index 703775bdd564..1bee4d3893a9 100644
--- a/core/modules/user/tests/src/Kernel/UserAccountFormPasswordResetTest.php
+++ b/core/modules/user/tests/src/Kernel/UserAccountFormPasswordResetTest.php
@@ -87,13 +87,14 @@ public function testPasswordResetToken() {
   protected function buildAccountForm($operation) {
     // @see HtmlEntityFormController::getFormObject()
     $entity_type = 'user';
-    $fields = [];
     if ($operation != 'register') {
-      $fields['uid'] = $this->user->id();
+      $entity = $this->user;
+    }
+    else {
+      $entity = $this->container->get('entity_type.manager')
+        ->getStorage($entity_type)
+        ->create();
     }
-    $entity = $this->container->get('entity_type.manager')
-      ->getStorage($entity_type)
-      ->create($fields);
 
     // @see EntityFormBuilder::getForm()
     return $this->container->get('entity.form_builder')->getForm($entity, $operation);
diff --git a/core/modules/user/tests/src/Kernel/UserAnonymousActivateTest.php b/core/modules/user/tests/src/Kernel/UserAnonymousActivateTest.php
new file mode 100644
index 000000000000..2aa2046e0f97
--- /dev/null
+++ b/core/modules/user/tests/src/Kernel/UserAnonymousActivateTest.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Tests\user\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests unblocking the anonymous user account.
+ *
+ * @group user
+ */
+class UserAnonymousActivateTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  protected static $modules = ['user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->container->get('module_handler')->loadInclude('user', 'install');
+    $this->installEntitySchema('user');
+    user_install();
+  }
+
+  /**
+   * Tests that the anonymous user cannot be activated.
+   */
+  public function testAnonymousActivate() {
+    $accountAnon = \Drupal::entityTypeManager()->getStorage('user')->load(0);
+
+    // Test that the anonymous user is blocked.
+    $this->assertTrue($accountAnon->isBlocked());
+
+    // Test that the anonymous user cannot be activated.
+    $this->expectException(\LogicException::class);
+    $this->expectExceptionMessage('The anonymous user account should remain blocked at all times.');
+    $accountAnon->activate();
+  }
+
+}
diff --git a/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php b/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
index 333d7bcff3ac..de7482a3ba2e 100644
--- a/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
+++ b/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
@@ -28,28 +28,28 @@ class UserAccessControlHandlerTest extends UnitTestCase {
   /**
    * The mock user account with view access.
    *
-   * @var \Drupal\Core\Session\AccountInterface
+   * @var \Drupal\user\UserInterface
    */
   protected $viewer;
 
   /**
    * The mock user account with 'view user email addresses' permission.
    *
-   * @var \Drupal\Core\Session\AccountInterface
+   * @var \Drupal\user\UserInterface
    */
   protected $emailViewer;
 
   /**
    * The mock user account that is able to change their own account name.
    *
-   * @var \Drupal\Core\Session\AccountInterface
+   * @var \Drupal\user\UserInterface
    */
   protected $owner;
 
   /**
    * The mock administrative test user.
    *
-   * @var \Drupal\Core\Session\AccountInterface
+   * @var \Drupal\user\UserInterface
    */
   protected $admin;
 
@@ -73,7 +73,7 @@ protected function setUp(): void {
     $container->set('cache_contexts_manager', $cache_contexts_manager);
     \Drupal::setContainer($container);
 
-    $this->viewer = $this->createMock('\Drupal\Core\Session\AccountInterface');
+    $this->viewer = $this->createMock('\Drupal\user\UserInterface');
     $this->viewer
       ->expects($this->any())
       ->method('hasPermission')
@@ -83,7 +83,7 @@ protected function setUp(): void {
       ->method('id')
       ->willReturn(1);
 
-    $this->owner = $this->createMock('\Drupal\Core\Session\AccountInterface');
+    $this->owner = $this->createMock('\Drupal\user\UserInterface');
     $this->owner
       ->expects($this->any())
       ->method('hasPermission')
@@ -97,13 +97,13 @@ protected function setUp(): void {
       ->method('id')
       ->willReturn(2);
 
-    $this->admin = $this->createMock('\Drupal\Core\Session\AccountInterface');
+    $this->admin = $this->createMock('\Drupal\user\UserInterface');
     $this->admin
       ->expects($this->any())
       ->method('hasPermission')
       ->willReturn(TRUE);
 
-    $this->emailViewer = $this->createMock('\Drupal\Core\Session\AccountInterface');
+    $this->emailViewer = $this->createMock('\Drupal\user\UserInterface');
     $this->emailViewer
       ->expects($this->any())
       ->method('hasPermission')
-- 
GitLab