diff --git a/lib/Drupal/masquerade/Tests/MasqueradeAccessTest.php b/lib/Drupal/masquerade/Tests/MasqueradeAccessTest.php new file mode 100644 index 0000000..0195f7c --- /dev/null +++ b/lib/Drupal/masquerade/Tests/MasqueradeAccessTest.php @@ -0,0 +1,187 @@ + 'Masquerade Access', + 'description' => 'Tests masquerade access mechanism.', + 'group' => 'Masquerade', + ); + } + + function setUp() { + parent::setUp(); + + // Create and configure User module's admin role. + // Users in this role get all permissions assigned by default. + $this->admin_role = entity_create('user_role', array( + 'id' => 'administrator', + 'label' => 'Administrator', + )); + $this->admin_role->save(); + config('user.settings')->set('admin_role', $this->admin_role->id())->save(); + user_role_grant_permissions($this->admin_role->id(), array_keys(module_invoke_all('permission'))); + + // Create an additional 'moderator' role with some typical permissions. + $this->moderator_role = entity_create('user_role', array( + 'id' => 'moderator', + 'label' => 'Moderator', + )); + $this->moderator_role->save(); + user_role_grant_permissions($this->moderator_role->id(), array_keys(user_permission())); + user_role_grant_permissions($this->moderator_role->id(), array_keys(comment_permission())); + + // Create an additional 'editor' role with some typical permissions. + $this->editor_role = entity_create('user_role', array( + 'id' => 'editor', + 'label' => 'Editor', + )); + $this->editor_role->save(); + user_role_grant_permissions($this->editor_role->id(), array_keys(node_permission())); + + // Create a 'masquerade' role. + $this->masquerade_role = entity_create('user_role', array( + 'id' => 'masquerade', + 'label' => 'Masquerade', + )); + $this->masquerade_role->save(); + user_role_grant_permissions($this->masquerade_role->id(), array('masquerade')); + + // Create test users with varying privilege levels: + // uid 1. + //$this->root_user + + // Administrative user with User module's admin role *only*. + $this->admin_user = $this->drupalCreateUser(); + $this->admin_user->name = 'admin_user'; + $this->admin_user->roles[$this->admin_role->id()] = $this->admin_role->id(); + $this->admin_user->save(); + + // Moderator user. + $this->moderator_user = $this->drupalCreateUser(); + $this->moderator_user->name = 'moderator_user'; + $this->moderator_user->roles[$this->moderator_role->id()] = $this->moderator_role->id(); + $this->moderator_user->roles[$this->masquerade_role->id()] = $this->masquerade_role->id(); + $this->moderator_user->save(); + + // Editor user. + $this->editor_user = $this->drupalCreateUser(); + $this->editor_user->name = 'editor_user'; + $this->editor_user->roles[$this->editor_role->id()] = $this->editor_role->id(); + $this->editor_user->roles[$this->masquerade_role->id()] = $this->masquerade_role->id(); + $this->editor_user->save(); + + // Masquerade user. + $this->masquerade_user = $this->drupalCreateUser(); + $this->masquerade_user->name = 'masquerade_user'; + $this->masquerade_user->roles[$this->masquerade_role->id()] = $this->masquerade_role->id(); + $this->masquerade_user->save(); + + // Authenticated user. + $this->auth_user = $this->drupalCreateUser(); + } + + /** + * Tests masquerade access for different source and target users. + * + * Test plan summary: + * - root » admin + * - admin ! root + * - admin » moderator (more roles but less privileges) + * - admin » masquerade (different role) + * - admin » auth (less roles) + * - moderator ! root + * - moderator ! admin (less roles but more privileges) + * - moderator ! editor (different roles + privileges) + * - moderator » masquerade (less roles) + * - moderator » auth + * - [editor is access-logic-wise equal to moderator, so skipped] + * - masquerade ! root + * - masquerade ! admin (different role with more privileges) + * - masquerade ! moderator (more roles) + * - masquerade » auth + * - masquerade ! masquerade (self) + * - auth ! * + */ + function testAccess() { + $this->drupalLogin($this->root_user); + $this->assertCanMasqueradeAs($this->admin_user); + + $this->drupalLogin($this->admin_user); + $this->assertCanNotMasqueradeAs($this->root_user); + $this->assertCanMasqueradeAs($this->moderator_user); + $this->assertCanMasqueradeAs($this->masquerade_user); + $this->assertCanMasqueradeAs($this->auth_user); + + $this->drupalLogin($this->moderator_user); + $this->assertCanNotMasqueradeAs($this->root_user); + $this->assertCanNotMasqueradeAs($this->admin_user); + $this->assertCanNotMasqueradeAs($this->editor_user); + $this->assertCanMasqueradeAs($this->masquerade_user); + $this->assertCanMasqueradeAs($this->auth_user); + + $this->drupalLogin($this->masquerade_user); + $this->assertCanNotMasqueradeAs($this->root_user); + $this->assertCanNotMasqueradeAs($this->admin_user); + $this->assertCanNotMasqueradeAs($this->moderator_user); + $this->assertCanMasqueradeAs($this->auth_user); + + // Verify that a user cannot masquerade as himself. + $edit = array( + 'masquerade_as' => $this->masquerade_user->name, + ); + $this->drupalPost('masquerade', $edit, t('Switch')); + $this->assertRaw(t('You cannot masquerade as yourself. Please choose a different user to masquerade as.')); + $this->assertNoText(t('Unmasquerade')); + + // Basic 'masquerade' permission check. + $this->drupalLogin($this->auth_user); + $this->drupalGet('masquerade'); + $this->assertResponse(403); + } + + /** + * Asserts that the currently logged-in user can masquerade as a given target user. + */ + protected function assertCanMasqueradeAs($target_account) { + $edit = array( + 'masquerade_as' => $target_account->name, + ); + $this->drupalPost('masquerade', $edit, t('Switch')); + $this->assertNoRaw(t('You are not allowed to masquerade as %name.', array( + '%name' => $target_account->name, + ))); + $this->clickLink(t('Unmasquerade')); + } + + /** + * Asserts that the currently logged-in user can not masquerade as a given target user. + */ + protected function assertCanNotMasqueradeAs($target_account) { + $edit = array( + 'masquerade_as' => $target_account->name, + ); + $this->drupalPost('masquerade', $edit, t('Switch')); + $this->assertRaw(t('You are not allowed to masquerade as %name.', array( + '%name' => $target_account->name, + ))); + $this->assertNoText(t('Unmasquerade')); + } + +} diff --git a/lib/Drupal/masquerade/Tests/MasqueradeTest.php b/lib/Drupal/masquerade/Tests/MasqueradeTest.php index a9fc78f..0e3999d 100644 --- a/lib/Drupal/masquerade/Tests/MasqueradeTest.php +++ b/lib/Drupal/masquerade/Tests/MasqueradeTest.php @@ -2,24 +2,15 @@ /** * @file - * Contains Drupal\masquerade\Tests\MasqueradeTest. + * Contains \Drupal\masquerade\Tests\MasqueradeTest. */ namespace Drupal\masquerade\Tests; -use Drupal\simpletest\WebTestBase; -use Drupal\user\Plugin\Core\Entity\User; - /** * Tests form permissions and user switching functionality. - * - * @todo Core: $this->session_id is reset to NULL upon every internal browser - * request. - * @see http://drupal.org/node/1555862 */ -class MasqueradeTest extends WebTestBase { - - public static $modules = array('masquerade'); +class MasqueradeTest extends MasqueradeWebTestBase { public static function getInfo() { return array( @@ -65,144 +56,5 @@ class MasqueradeTest extends WebTestBase { $this->assertSessionByUid($this->admin_user->uid, FALSE); } - /** - * Masquerades as another user. - * - * @param \Drupal\user\Plugin\Core\Entity\User $account - * The user account to masquerade as. - */ - protected function masqueradeAs(User $account) { - $this->drupalGet('user/' . $account->uid . '/masquerade', array( - 'query' => array( - 'token' => $this->drupalGetToken('user/' . $account->uid . '/masquerade'), - ), - )); - $this->assertResponse(200); - $this->assertText('You are now masquerading as ' . $account->label()); - - // Update the logged in user account. - // @see WebTestBase::drupalLogin() - if (isset($this->session_id)) { - $this->loggedInUser = $account; - $this->loggedInUser->session_id = $this->session_id; - } - } - - /** - * Unmasquerades the current user. - * - * @param \Drupal\user\Plugin\Core\Entity\User $account - * The user account to unmasquerade from. - */ - protected function unmasquerade(User $account) { - $this->drupalGet('unmasquerade', array( - 'query' => array( - 'token' => $this->drupalGetToken('unmasquerade'), - ), - )); - $this->assertResponse(200); - $this->assertText('You are no longer masquerading as ' . $account->label()); - - // Update the logged in user account. - // @see WebTestBase::drupalLogin() - if (isset($this->session_id)) { - $this->loggedInUser = $account; - $this->loggedInUser->session_id = $this->session_id; - } - } - - /** - * Asserts that there is a session for a given user ID. - * - * @param int $uid - * The user ID for which to find a session record. - * @param int|false $expected_masquerading_uid - * (optional) The expected value of the 'masquerading' session data. Pass - * FALSE to assert that the session data is not set. - * - * @return stdClass - * The session record from {sessions}, if any. - */ - protected function assertSessionByUid($uid, $expected_masquerading_uid = NULL) { - $result = db_query('SELECT * FROM {sessions} WHERE uid = :uid', array( - ':uid' => $uid, - ))->fetchAll(); - // If there is more than one session, then that must be unexpected. - if (count($result) > 1) { - $this->fail("Found more than 1 session for uid $uid."); - } - else { - $this->pass("Found session for uid $uid."); - $session = reset($result); - - // Decode the session data. - if (!empty($session->session)) { - // Careful: PHP does not provide a utility function that decodes session - // data only. session_decode() merges the input into the global - // $_SESSION (but only if it is an array). - // @see http://php.net/manual/function.session-decode.php - $old_session = isset($_SESSION) ? $_SESSION : NULL; - // Furthermore, if this test is executed on the command line, then - // Drupal denies to start a session. PHP throws a notice if the session - // is attempted to be started more than once. - // @see drupal_session_start() - @session_start(); - // In any case, ensure that it is empty. - $_SESSION = array(); - - if (!session_decode($session->session)) { - $this->fail(format_string('Failed to decode session data: @data', array('@data' => $session->session))); - } - $session->session = isset($_SESSION) ? $_SESSION : array(); - - // Restore the original global session. - $_SESSION = NULL; - if (isset($old_session)) { - $_SESSION = $old_session; - } - } - else { - $session->session = array(); - } - - if (isset($expected_masquerading_uid)) { - if ($expected_masquerading_uid !== FALSE) { - $this->assertEqual($session->session['masquerading'], $expected_masquerading_uid, format_string('$_SESSION[\'masquerading\'] equals @uid.', array( - '@uid' => $expected_masquerading_uid, - ))); - } - else { - $this->assert(!isset($session->session['masquerading']), '$_SESSION[\'masquerading\'] is not set.'); - } - } - return $session; - } - } - - /** - * Asserts that no session exists for a given uid. - * - * @param int $uid - * The user ID to assert. - */ - protected function assertNoSessionByUid($uid) { - $result = db_query('SELECT * FROM {sessions} WHERE uid = :uid', array( - ':uid' => $uid, - ))->fetchAll(); - $this->assert(empty($result), "No session for uid $uid found."); - } - - /** - * Stop-gap fix. - * - * @see http://drupal.org/node/1555862 - */ - protected function drupalGetToken($value = '') { - $private_key = drupal_get_private_key(); - // Use the session_id assigned by WebTestBase::drupalLogin() instead of - // $this->session_id until the core bug is fixed. - $session_id = isset($this->loggedInUser->session_id) ? $this->loggedInUser->session_id : ''; - return drupal_hmac_base64($value, $session_id . $private_key . drupal_get_hash_salt()); - } } diff --git a/lib/Drupal/masquerade/Tests/MasqueradeTest.php b/lib/Drupal/masquerade/Tests/MasqueradeWebTestBase.php similarity index 75% copy from lib/Drupal/masquerade/Tests/MasqueradeTest.php copy to lib/Drupal/masquerade/Tests/MasqueradeWebTestBase.php index a9fc78f..70525b0 100644 --- a/lib/Drupal/masquerade/Tests/MasqueradeTest.php +++ b/lib/Drupal/masquerade/Tests/MasqueradeWebTestBase.php @@ -2,7 +2,7 @@ /** * @file - * Contains Drupal\masquerade\Tests\MasqueradeTest. + * Contains \Drupal\masquerade\Tests\MasqueradeWebTestBase. */ namespace Drupal\masquerade\Tests; @@ -11,60 +11,16 @@ use Drupal\simpletest\WebTestBase; use Drupal\user\Plugin\Core\Entity\User; /** - * Tests form permissions and user switching functionality. + * Base test class for Masquerade module web tests. * * @todo Core: $this->session_id is reset to NULL upon every internal browser * request. * @see http://drupal.org/node/1555862 */ -class MasqueradeTest extends WebTestBase { +abstract class MasqueradeWebTestBase extends WebTestBase { public static $modules = array('masquerade'); - public static function getInfo() { - return array( - 'name' => 'Masquerade functionality', - 'description' => 'Tests form permissions and user switching functionality.', - 'group' => 'Masquerade', - ); - } - - function testMasquerade() { - $this->admin_user = $this->drupalCreateUser(array('masquerade')); - $this->web_user = $this->drupalCreateUser(); - - $this->drupalLogin($this->admin_user); - - // Verify that a token is required. - $this->drupalGet('user/0/masquerade'); - $this->assertResponse(403); - $this->drupalGet('user/' . $this->web_user->uid . '/masquerade'); - $this->assertResponse(403); - - // Verify that the admin user is able to masquerade. - $this->assertSessionByUid($this->admin_user->uid, FALSE); - $this->masqueradeAs($this->web_user); - $this->assertSessionByUid($this->web_user->uid, $this->admin_user->uid); - $this->assertNoSessionByUid($this->admin_user->uid); - - // Verify that a token is required to unmasquerade. - $this->drupalGet('unmasquerade'); - $this->assertResponse(403); - - // Verify that the web user cannot masquerade. - $this->drupalGet('user/' . $this->admin_user->uid . '/masquerade', array( - 'query' => array( - 'token' => $this->drupalGetToken('user/' . $this->admin_user->uid . '/masquerade'), - ), - )); - $this->assertResponse(403); - - // Verify that the user can unmasquerade. - $this->unmasquerade($this->web_user); - $this->assertNoSessionByUid($this->web_user->uid); - $this->assertSessionByUid($this->admin_user->uid, FALSE); - } - /** * Masquerades as another user. * @@ -204,5 +160,6 @@ class MasqueradeTest extends WebTestBase { $session_id = isset($this->loggedInUser->session_id) ? $this->loggedInUser->session_id : ''; return drupal_hmac_base64($value, $session_id . $private_key . drupal_get_hash_salt()); } + } diff --git a/masquerade.module b/masquerade.module index b1493cf..c6935ff 100644 --- a/masquerade.module +++ b/masquerade.module @@ -63,6 +63,12 @@ function masquerade_permission() { * Implements hook_menu(). */ function masquerade_menu() { + $items['masquerade'] = array( + 'page callback' => 'drupal_get_form', + 'page arguments' => array('masquerade_block_form'), + 'access arguments' => array('masquerade'), + 'type' => MENU_CALLBACK, + ); $items['user/%user/masquerade'] = array( 'title' => 'Masquerade', 'page callback' => 'masquerade_switch_user_page', @@ -157,13 +163,46 @@ function masquerade_user_access(User $target_account) { /** * Implements hook_masquerade_access(). + * + * This default implementation only returns TRUE and ever FALSE, since + * alternative access implementations could not work otherwise. */ function masquerade_masquerade_access($user, User $target_account) { - // Only return TRUE, since alternative access implementations could not work - // otherwise. - // By default, access to masquerade as uid 1 is not granted (but also not - // denied, so other implementations may grant access). - if ($target_account->id() != 1 && user_access('masquerade')) { + // Uid 1 may masquerade as anyone. + if ($user->uid == 1) { + return TRUE; + } + // No one can masquerade as uid 1. + if ($target_account->id() == 1) { + return; + } + // The current user must be allowed to masquerade. + if (!user_access('masquerade')) { + return; + } + + // If the current user has the identical roles as the target user (or + // additional roles), access is granted. + // Note: array_diff*() returns all values from the first array that are NOT + // contained in the second. + $missing_roles = array_diff_assoc($target_account->roles, $user->roles); + if (!$missing_roles) { + return TRUE; + } + $additional_roles = array_diff_assoc($user->roles, $target_account->roles); + + // If the current user has the identical permissions as the target user (or + // additional permissions), access is granted. + $needs_permissions = array(); + foreach (user_role_permissions($missing_roles) as $rid => $permissions) { + $needs_permissions += $permissions; + } + $has_permissions = array(); + foreach (user_role_permissions($additional_roles) as $rid => $permissions) { + $has_permissions += $permissions; + } + $missing_permissions = array_diff_key($needs_permissions, $has_permissions); + if (!$missing_permissions) { return TRUE; } } @@ -203,7 +242,7 @@ function masquerade_user_view(User $account, $display, $view_mode, $langcode) { /** * Form constructor for the Masquerade block form. */ -function masquerade_block_form() { +function masquerade_block_form($form, &$form_state) { $form['autocomplete'] = array( '#type' => 'container', '#attributes' => array('class' => array('container-inline')),