API Allow users to act-as another

This commit is contained in:
Damian Mooyman 2017-01-13 16:11:20 +13:00
parent 5d4ab7f255
commit 7d67c5b9bd
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
4 changed files with 139 additions and 3 deletions

View File

@ -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

View File

@ -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;
});
}
/**

View File

@ -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.
* <code>
* Member::logInAs(Security::findAnAdministrator(), function() {
* $record->write();
* });
* </code>
*
* @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;

View File

@ -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);
}
}