Commit 8d80b7a4 authored by alexpott's avatar alexpott

Issue #2417895 by David_Rothstein, Berdir, Wim Leers, willzyx, catch:...

Issue #2417895 by David_Rothstein, Berdir, Wim Leers, willzyx, catch: AccountPermissionsCacheContext/PermissionsHashGenerator must special case user 1, since permissions don't apply to it
parent 42c693a9
......@@ -9,6 +9,9 @@
/**
* Defines the UserRolesCacheContext service, for "per role" caching.
*
* Only use this cache context when checking explicitly for certain roles. Use
* user.permissions for anything that checks permissions.
*/
class UserRolesCacheContext extends UserCacheContext implements CalculatedCacheContextInterface{
......@@ -23,6 +26,13 @@ public static function getLabel() {
* {@inheritdoc}
*/
public function getContext($role = NULL) {
// User 1 does not actually have any special behavior for roles, this is
// added as additional security and BC compatibility for SA-CORE-2015-002.
// user.
// @todo Remove in Drupal 9.0.0.
if ($this->user->id() == 1) {
return 'is-super-user';
}
if ($role === NULL) {
return 'r.' . implode(',', $this->user->getRoles());
}
......
......@@ -51,6 +51,12 @@ public function __construct(PrivateKey $private_key, CacheBackendInterface $cach
* Cached by role, invalidated whenever permissions change.
*/
public function generate(AccountInterface $account) {
// User 1 is the super user, and can always access all permissions. Use a
// different, unique identifier for the hash.
if ($account->id() == 1) {
return $this->hash('is-super-user');
}
$sorted_roles = $account->getRoles();
sort($sorted_roles);
$role_list = implode(',', $sorted_roles);
......@@ -81,9 +87,28 @@ protected function doGenerate(array $roles) {
$permissions_by_role = user_role_permissions($roles);
foreach ($permissions_by_role as $role => $permissions) {
sort($permissions);
// Note that for admin roles (\Drupal\user\RoleInterface::isAdmin()), the
// permissions returned will be empty ($permissions = []). Therefore the
// presence of the role ID as a key in $permissions_by_role is essential
// to ensure that the hash correctly recognizes admin roles. (If the hash
// was based solely on the union of $permissions, the admin roles would
// effectively be no-ops, allowing for hash collisions.)
$permissions_by_role[$role] = $permissions;
}
return hash('sha256', $this->privateKey->get() . Settings::getHashSalt() . serialize($permissions_by_role));
return $this->hash(serialize($permissions_by_role));
}
/**
* Hashes the given string.
*
* @param string $identifier
* The string to be hashed.
*
* @return string
* The hash.
*/
protected function hash($identifier) {
return hash('sha256', $this->privateKey->get() . Settings::getHashSalt() . $identifier);
}
}
<?php
/**
* @file
* Contains \Drupal\system\Tests\Render\RenderCacheTest.
*/
namespace Drupal\system\Tests\Render;
use Drupal\simpletest\KernelTestBase;
use Drupal\simpletest\UserCreationTrait;
/**
* Tests the caching of render items via functional tests.
*
* @group Render
*/
class RenderCacheTest extends KernelTestBase {
use UserCreationTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['user', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installConfig(['user']);
$this->installSchema('system', ['sequences']);
}
/**
* Tests that user 1 has a different permission context with the same roles.
*/
public function testUser1PermissionContext() {
$this->doTestUser1WithContexts(['user.permissions']);
}
/**
* Tests that user 1 has a different roles context with the same roles.
*/
public function testUser1RolesContext() {
$this->doTestUser1WithContexts(['user.roles']);
}
/**
* Ensures that user 1 has a unique render cache for the given context.
*
* @param string[] $contexts
* List of cache contexts to use.
*/
protected function doTestUser1WithContexts($contexts) {
// Test that user 1 does not share the cache with other users who have the
// same roles, even when using a role-based cache context.
$user1 = $this->createUser();
$this->assertEqual($user1->id(), 1);
$first_authenticated_user = $this->createUser();
$second_authenticated_user = $this->createUser();
$admin_user = $this->createUser([], NULL, TRUE);
$this->assertEqual($user1->getRoles(), $first_authenticated_user->getRoles(), 'User 1 has the same roles as an authenticated user.');
// Impersonate user 1 and render content that only user 1 should have
// permission to see.
\Drupal::service('account_switcher')->switchTo($user1);
$test_element = [
'#cache' => [
'keys' => ['test'],
'contexts' => $contexts,
],
];
$element = $test_element;
$element['#markup'] = 'content for user 1';
$output = \Drupal::service('renderer')->render($element);
$this->assertEqual($output, 'content for user 1');
// Verify the cache is working by rendering the same element but with
// different markup passed in; the result should be the same.
$element = $test_element;
$element['#markup'] = 'should not be used';
$output = \Drupal::service('renderer')->render($element);
$this->assertEqual($output, 'content for user 1');
\Drupal::service('account_switcher')->switchBack();
// Verify that the first authenticated user does not see the same content
// as user 1.
\Drupal::service('account_switcher')->switchTo($first_authenticated_user);
$element = $test_element;
$element['#markup'] = 'content for authenticated users';
$output = \Drupal::service('renderer')->render($element);
$this->assertEqual($output, 'content for authenticated users');
\Drupal::service('account_switcher')->switchBack();
// Verify that the second authenticated user shares the cache with the
// first authenticated user.
\Drupal::service('account_switcher')->switchTo($second_authenticated_user);
$element = $test_element;
$element['#markup'] = 'should not be used';
$output = \Drupal::service('renderer')->render($element);
$this->assertEqual($output, 'content for authenticated users');
\Drupal::service('account_switcher')->switchBack();
// Verify that the admin user (who has an admin role without explicit
// permissions) does not share the same cache.
\Drupal::service('account_switcher')->switchTo($admin_user);
$element = $test_element;
$element['#markup'] = 'content for admin user';
$output = \Drupal::service('renderer')->render($element);
$this->assertEqual($output, 'content for admin user');
\Drupal::service('account_switcher')->switchBack();
}
}
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\Tests\Core\Session\PermissionsHashTest.
* Contains \Drupal\Tests\Core\Session\PermissionsHashGeneratorTest.
*/
namespace Drupal\Tests\Core\Session {
......@@ -17,28 +17,35 @@
* @coversDefaultClass \Drupal\Core\Session\PermissionsHashGenerator
* @group Session
*/
class PermissionsHashTest extends UnitTestCase {
class PermissionsHashGeneratorTest extends UnitTestCase {
/**
* A mocked account.
* The mocked super user account.
*
* @var \Drupal\user\UserInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $account1;
/**
* A mocked account.
*
* @var \Drupal\user\UserInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $account2;
/**
* An "updated" mocked account.
*
* @var \Drupal\user\UserInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $account1Updated;
protected $account2Updated;
/**
* A different account.
*
* @var \Drupal\user\UserInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $account2;
protected $account3;
/**
* The mocked private key service.
......@@ -69,35 +76,55 @@ protected function setUp() {
new Settings(array('hash_salt' => 'test'));
// Account 1: 'administrator' and 'authenticated' roles.
$roles_1 = array('administrator', 'authenticated');
// The mocked super user account, with the same roles as Account 2.
$this->account1 = $this->getMockBuilder('Drupal\user\Entity\User')
->disableOriginalConstructor()
->setMethods(array('getRoles'))
->setMethods(array('getRoles', 'id'))
->getMock();
$this->account1->expects($this->any())
->method('getRoles')
->will($this->returnValue($roles_1));
->method('id')
->willReturn(1);
$this->account1->expects($this->never())
->method('getRoles');
// Account 2: 'authenticated' and 'administrator' roles (different order).
$roles_2 = array('authenticated', 'administrator');
// Account 2: 'administrator' and 'authenticated' roles.
$roles_1 = array('administrator', 'authenticated');
$this->account2 = $this->getMockBuilder('Drupal\user\Entity\User')
->disableOriginalConstructor()
->setMethods(array('getRoles'))
->setMethods(array('getRoles', 'id'))
->getMock();
$this->account2->expects($this->any())
->method('getRoles')
->will($this->returnValue($roles_2));
->will($this->returnValue($roles_1));
$this->account2->expects($this->any())
->method('id')
->willReturn(2);
// Updated account 1: now also 'editor' role.
$roles_1_updated = array('editor', 'administrator', 'authenticated');
$this->account1Updated = $this->getMockBuilder('Drupal\user\Entity\User')
// Account 3: 'authenticated' and 'administrator' roles (different order).
$roles_3 = array('authenticated', 'administrator');
$this->account3 = $this->getMockBuilder('Drupal\user\Entity\User')
->disableOriginalConstructor()
->setMethods(array('getRoles'))
->setMethods(array('getRoles', 'id'))
->getMock();
$this->account1Updated->expects($this->any())
$this->account3->expects($this->any())
->method('getRoles')
->will($this->returnValue($roles_1_updated));
->will($this->returnValue($roles_3));
$this->account3->expects($this->any())
->method('id')
->willReturn(3);
// Updated account 2: now also 'editor' role.
$roles_2_updated = array('editor', 'administrator', 'authenticated');
$this->account2Updated = $this->getMockBuilder('Drupal\user\Entity\User')
->disableOriginalConstructor()
->setMethods(array('getRoles', 'id'))
->getMock();
$this->account2Updated->expects($this->any())
->method('getRoles')
->will($this->returnValue($roles_2_updated));
$this->account2Updated->expects($this->any())
->method('id')
->willReturn(2);
// Mocked private key + cache services.
$random = Crypt::randomBytesBase64(55);
......@@ -119,14 +146,19 @@ protected function setUp() {
* Tests the generate() method.
*/
public function testGenerate() {
// Ensure that the super user (user 1) always gets the same hash.
$super_user_hash = $this->permissionsHash->generate($this->account1);
// Ensure that two user accounts with the same roles generate the same hash.
$hash_1 = $this->permissionsHash->generate($this->account1);
$hash_2 = $this->permissionsHash->generate($this->account2);
$this->assertSame($hash_1, $hash_2, 'Different users with the same roles generate the same permissions hash.');
$hash_3 = $this->permissionsHash->generate($this->account3);
$this->assertSame($hash_2, $hash_3, 'Different users with the same roles generate the same permissions hash.');
$this->assertNotSame($hash_2, $super_user_hash, 'User 1 has a different hash despite having the same roles');
// Compare with hash for user account 1 with an additional role.
$updated_hash_1 = $this->permissionsHash->generate($this->account1Updated);
$this->assertNotSame($hash_1, $updated_hash_1, 'Same user with updated roles generates different permissions hash.');
$updated_hash_2 = $this->permissionsHash->generate($this->account2Updated);
$this->assertNotSame($hash_2, $updated_hash_2, 'Same user with updated roles generates different permissions hash.');
}
/**
......@@ -146,7 +178,7 @@ public function testGenerateCache() {
$this->cache->expects($this->never())
->method('set');
$this->permissionsHash->generate($this->account1);
$this->permissionsHash->generate($this->account2);
}
/**
......@@ -164,7 +196,7 @@ public function testGenerateNoCache() {
->method('set')
->with($expected_cid, $this->isType('string'));
$this->permissionsHash->generate($this->account1);
$this->permissionsHash->generate($this->account2);
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment