NEW Add sudo mode service

This commit is contained in:
Guy Sartorelli 2023-01-30 12:51:38 +13:00
parent 8ddedb038e
commit fecb7ba4d8
No known key found for this signature in database
GPG Key ID: F313E3B9504D496A
5 changed files with 180 additions and 1 deletions

View File

@ -43,6 +43,8 @@ SilverStripe\Core\Injector\Injector:
Authenticators: Authenticators:
cms: '%$SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator' cms: '%$SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator'
SilverStripe\Security\IdentityStore: '%$SilverStripe\Security\AuthenticationHandler' SilverStripe\Security\IdentityStore: '%$SilverStripe\Security\AuthenticationHandler'
SilverStripe\Security\SudoMode\SudoModeServiceInterface:
class: SilverStripe\Security\SudoMode\SudoModeService
SilverStripe\Security\PasswordExpirationMiddleware: SilverStripe\Security\PasswordExpirationMiddleware:
default_redirect: Security/changepassword default_redirect: Security/changepassword

View File

@ -6,8 +6,11 @@ use SilverStripe\Control\Controller;
use SilverStripe\Control\Cookie; use SilverStripe\Control\Cookie;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Security\AuthenticationHandler; use SilverStripe\Security\AuthenticationHandler;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\SudoMode\SudoModeServiceInterface;
/** /**
* Authenticate a member passed on a session cookie * Authenticate a member passed on a session cookie
@ -72,13 +75,20 @@ class SessionAuthenticationHandler implements AuthenticationHandler
{ {
static::regenerateSessionId(); static::regenerateSessionId();
$request = $request ?: Controller::curr()->getRequest(); $request = $request ?: Controller::curr()->getRequest();
$request->getSession()->set($this->getSessionVariable(), $member->ID); $session = $request->getSession();
$session->set($this->getSessionVariable(), $member->ID);
// This lets apache rules detect whether the user has logged in // This lets apache rules detect whether the user has logged in
// @todo make this a setting on the authentication handler // @todo make this a setting on the authentication handler
if (Member::config()->get('login_marker_cookie')) { if (Member::config()->get('login_marker_cookie')) {
Cookie::set(Member::config()->get('login_marker_cookie'), 1, 0); Cookie::set(Member::config()->get('login_marker_cookie'), 1, 0);
} }
// Activate sudo mode on login so the user doesn't have to reauthenticate for sudo
// actions until the sudo mode timeout expires
/** @var SudoModeServiceInterface $service */
$service = Injector::inst()->get(SudoModeServiceInterface::class);
$service->activate($session);
} }
/** /**

View File

@ -0,0 +1,52 @@
<?php
namespace SilverStripe\Security\SudoMode;
use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\ORM\FieldType\DBDatetime;
class SudoModeService implements SudoModeServiceInterface
{
use Configurable;
/**
* The lifetime that sudo mode authorization lasts for, in minutes.
*
* Note that if the PHP session times out before this lifetime is reached, it will automatically be reset.
* @see \SilverStripe\Control\Session::$timeout
*/
private static int $lifetime_minutes = 45;
/**
* The session key that is used to store the timestamp for when sudo mode was last activated
*
* @var string
*/
private const SUDO_MODE_SESSION_KEY = 'sudo-mode-last-activated';
public function check(Session $session): bool
{
$lastActivated = $session->get(self::SUDO_MODE_SESSION_KEY);
// Not activated at all
if (!$lastActivated) {
return false;
}
// Activated within the last "lifetime" window
$nowTimestamp = DBDatetime::now()->getTimestamp();
return $lastActivated > ($nowTimestamp - $this->getLifetime() * 60);
}
public function activate(Session $session): bool
{
$session->set(self::SUDO_MODE_SESSION_KEY, DBDatetime::now()->getTimestamp());
return true;
}
public function getLifetime(): int
{
return (int) static::config()->get('lifetime_minutes');
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace SilverStripe\Security\SudoMode;
use SilverStripe\Control\Session;
/**
* A service class responsible for activating and checking the current status of elevated permission levels
* via "sudo mode". This is done by checking a timestamp value in the provided session.
*/
interface SudoModeServiceInterface
{
/**
* Checks the current session to see if sudo mode was activated within the last section of lifetime allocation.
*
* @return true if sudo mode is currently active
*/
public function check(Session $session): bool;
/**
* Register activated sudo mode permission in the provided session, which lasts for the configured lifetime.
*
* @return true on success
*/
public function activate(Session $session): bool;
/**
* How long the sudo mode activation lasts for in minutes.
*/
public function getLifetime(): int;
}

View File

@ -0,0 +1,84 @@
<?php
namespace SilverStripe\Security\SudoMode\Tests;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\SudoMode\SudoModeService;
use SilverStripe\Security\SudoMode\SudoModeServiceInterface;
class SudoModeServiceTest extends SapphireTest
{
/**
* @var Session
*/
private $session;
/**
* @var SudoModeService
*/
private $service;
protected function setUp(): void
{
parent::setUp();
$this->session = new Session([]);
$this->service = new SudoModeService();
DBDatetime::set_mock_now('2019-03-01 12:00:00');
SudoModeService::config()->set('lifetime_minutes', 180);
}
public function testCheckWithoutActivation()
{
$this->session->clearAll();
$this->assertFalse($this->service->check($this->session));
}
public function testCheckWithLastActivationOutsideLifetimeWindow()
{
// 240 minutes ago
$lastActivated = DBDatetime::now()->getTimestamp() - 240 * 60;
$this->session->set('sudo-mode-last-activated', $lastActivated);
$this->assertFalse($this->service->check($this->session));
}
public function testCheckWithLastActivationInsideLifetimeWindow()
{
// 25 minutes ago
$lastActivated = DBDatetime::now()->getTimestamp() - 25 * 60;
$this->session->set('sudo-mode-last-activated', $lastActivated);
$this->assertTrue($this->service->check($this->session));
}
public function testActivateAndCheckImmediately()
{
$this->service->activate($this->session);
$this->assertTrue($this->service->check($this->session));
}
public function testSudoModeActivatesOnLogin()
{
// Sometimes being logged in carries over from other tests
$this->logOut();
/** @var SudoModeServiceInterface $service */
$service = Injector::inst()->get(SudoModeServiceInterface::class);
$session = Controller::curr()->getRequest()->getSession();
// Sudo mode should not be enabled automagically when nobody is logged in
$this->assertFalse($service->check($session));
// Ensure sudo mode is activated on login
$this->logInWithPermission();
$this->assertTrue($service->check($session));
// Ensure sudo mode is not active after logging out
$this->logOut();
$this->assertFalse($service->check($session));
}
}