diff --git a/docs/en/02_Developer_Guides/09_Security/00_Member.md b/docs/en/02_Developer_Guides/09_Security/00_Member.md index 41d801e96..6bca454c9 100644 --- a/docs/en/02_Developer_Guides/09_Security/00_Member.md +++ b/docs/en/02_Developer_Guides/09_Security/00_Member.md @@ -134,6 +134,39 @@ will be created and associated with the device used during authentication. When for all devices will be revoked, unless `[api:RememberLoginHash::$logout_across_devices] is set to false. For extra security, single tokens can be enforced by setting `[api:RememberLoginHash::$force_single_token] to true. +## Acting as another user ## + +Occasionally, it may be necessary not only to check permissions of a particular member, but also to +temporarily assume the identity of another user for certain tasks. E.g. when running a CLI task, +it may be necessary to log in as an administrator to perform write operations. + +You can use `Member::actAs()` method, which takes a member or member id to act as, and a callback +within which the current user will be assigned the given member. After this method returns +the current state will be restored to whichever current user (if any) was logged in. + +If you pass in null as a first argument, you can also mock being logged out, without modifying +the current user. + +Note: Take care not to invoke this method to perform any operation the current user should not +reasonably be expected to be allowed to do. + +E.g. + + + :::php + class CleanRecordsTask extends BuildTask + { + public function run($request) + { + if (!Director::is_cli()) { + throw new BadMethodCallException('This task only runs on CLI'); + } + $admin = Security::findAnAdministrator(); + Member::actAs($admin, function() { + DataRecord::get()->filter('Dirty', true)->removeAll(); + }); + } + ## API Documentation diff --git a/src/Assets/AssetControlExtension.php b/src/Assets/AssetControlExtension.php index cda497a82..4a6d9f731 100644 --- a/src/Assets/AssetControlExtension.php +++ b/src/Assets/AssetControlExtension.php @@ -111,9 +111,11 @@ class AssetControlExtension extends DataExtension } // Check if canView permits anonymous viewers - return $record->canView(Member::create()) - ? AssetManipulationList::STATE_PUBLIC - : AssetManipulationList::STATE_PROTECTED; + return Member::actAs(null, function () use ($record) { + return $record->canView() + ? AssetManipulationList::STATE_PUBLIC + : AssetManipulationList::STATE_PROTECTED; + }); } /** diff --git a/src/Security/Member.php b/src/Security/Member.php index 129adb8e9..77f8c0d9c 100644 --- a/src/Security/Member.php +++ b/src/Security/Member.php @@ -871,6 +871,40 @@ class Member extends DataObject implements TemplateGlobalProvider } } + /** + * Allow override of the current user ID + * + * @var int|null Set to null to fallback to session, or an explicit ID + */ + protected static $overrideID = null; + + /** + * Temporarily act as the specified user, limited to a $callback, but + * without logging in as that user. + * + * E.g. + * + * Member::logInAs(Security::findAnAdministrator(), function() { + * $record->write(); + * }); + * + * + * @param Member|null|int $member Member or member ID to log in as. + * Set to null or 0 to act as a logged out user. + * @param $callback + */ + public static function actAs($member, $callback) + { + $id = ($member instanceof Member ? $member->ID : $member) ?: 0; + $previousID = static::$overrideID; + static::$overrideID = $id; + try { + return $callback(); + } finally { + static::$overrideID = $previousID; + } + } + /** * Get the ID of the current logged in user * @@ -878,6 +912,10 @@ class Member extends DataObject implements TemplateGlobalProvider */ public static function currentUserID() { + if (isset(static::$overrideID)) { + return static::$overrideID; + } + $id = Session::get("loggedInAs"); if (!$id && !self::$_already_tried_to_auto_log_in) { self::autoLogin(); @@ -886,6 +924,7 @@ class Member extends DataObject implements TemplateGlobalProvider return is_numeric($id) ? $id : 0; } + private static $_already_tried_to_auto_log_in = false; diff --git a/tests/php/Security/MemberTest.php b/tests/php/Security/MemberTest.php index f68573bd1..12b0a8b96 100644 --- a/tests/php/Security/MemberTest.php +++ b/tests/php/Security/MemberTest.php @@ -1402,4 +1402,66 @@ class MemberTest extends FunctionalTest $userFromSession = Member::currentUser(); $this->assertEquals($adminMember->ID, $userFromSession->ID); } + + /** + * @covers \SilverStripe\Security\Member::actAs() + */ + public function testActAsUserPermissions() + { + $this->assertNull(Member::currentUser()); + + /** @var Member $adminMember */ + $adminMember = $this->objFromFixture(Member::class, 'admin'); + + // Check acting as admin when not logged in + $checkAdmin = Member::actAs($adminMember, function () { + return Permission::check('ADMIN'); + }); + $this->assertTrue($checkAdmin); + + // Check nesting + $checkAdmin = Member::actAs($adminMember, function () { + return Member::actAs(null, function () { + return Permission::check('ADMIN'); + }); + }); + $this->assertFalse($checkAdmin); + + // Check logging in as non-admin user + $this->logInWithPermission('TEST_PERMISSION'); + + $hasPerm = Member::actAs(null, function () { + return Permission::check('TEST_PERMISSION'); + }); + $this->assertFalse($hasPerm); + + // Check permissions can be promoted + $checkAdmin = Member::actAs($adminMember, function () { + return Permission::check('ADMIN'); + }); + $this->assertTrue($checkAdmin); + } + + /** + * @covers \SilverStripe\Security\Member::actAs() + */ + public function testActAsUser() + { + $this->assertNull(Member::currentUser()); + + /** @var Member $adminMember */ + $adminMember = $this->objFromFixture(Member::class, 'admin'); + $memberID = Member::actAs($adminMember, function () { + return Member::currentUserID(); + }); + $this->assertEquals($adminMember->ID, $memberID); + + // Check nesting + $memberID = Member::actAs($adminMember, function () { + return Member::actAs(null, function () { + return Member::currentUserID(); + }); + }); + $this->assertEmpty($memberID); + } }