Skip to content

Commit

Permalink
Merge pull request #3648 from neos/feature/impersonation
Browse files Browse the repository at this point in the history
FEATURE: Implement user impersonation
  • Loading branch information
kdambekalns committed Mar 24, 2022
2 parents d47bed0 + 0fe1e20 commit ed81472
Show file tree
Hide file tree
Showing 17 changed files with 830 additions and 5 deletions.
202 changes: 202 additions & 0 deletions Neos.Neos/Classes/Controller/Backend/ImpersonateController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);

namespace Neos\Neos\Controller\Backend;

/*
* This file is part of the Neos.Neos package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\Mvc\Exception\StopActionException;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Neos\Flow\Mvc\View\JsonView;
use Neos\Flow\Security\Account;
use Neos\Neos\Domain\Model\User;
use Neos\Party\Domain\Service\PartyService;
use Neos\Neos\Service\ImpersonateService;

/**
* The Impersonate controller
*
* @Flow\Scope("singleton")
*/
class ImpersonateController extends ActionController
{
/**
* @var ImpersonateService
* @Flow\Inject
*/
protected $impersonateService;

/**
* @var PartyService
* @Flow\Inject
*/
protected $partyService;

/**
* @var string
*/
protected $defaultViewObjectName = JsonView::class;

/**
* @var array
*/
protected $viewFormatToObjectNameMap = [
'json' => JsonView::class
];

/**
* @var array
*/
protected $supportedMediaTypes = [
'application/json'
];

public function impersonateAction(Account $account): void
{
$this->impersonateService->impersonate($account);
$this->redirectIfPossible('impersonate');
}

/**
* Fetching possible redirect options for the given action method and if everything is set we redirect to the
* configured controller action.
*/
protected function redirectIfPossible(string $actionName): void
{
$action = $this->settings['redirectOptions'][$actionName]['action'] ?? '';
$controller = $this->settings['redirectOptions'][$actionName]['controller'] ?? '';
$package = $this->settings['redirectOptions'][$actionName]['package'] ?? '';

if ($action !== '' && $controller !== '' && $package !== '' && $this->impersonateService->getImpersonation() === null) {
$this->redirectWithParentRequest($action, $controller, $package);
}
}

/**
* @param string $actionName Name of the action to forward to
* @param string $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used.
* @param string $packageKey Key of the package containing the controller to forward to. If not specified, the current package is assumed.
* @param array $arguments Array of arguments for the target action
* @param integer $delay (optional) The delay in seconds. Default is no delay.
* @param integer $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other"
* @param string $format The format to use for the redirect URI
* @see redirect()
*/
protected function redirectWithParentRequest(string $actionName, string $controllerName = null, string $packageKey = null, array $arguments = [], int $delay = 0, int $statusCode = 303, string $format = null): void
{
$request = $this->getControllerContext()->getRequest()->getMainRequest();
$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($request);

if ($packageKey !== null && strpos($packageKey, '\\') !== false) {
list($packageKey, $subpackageKey) = explode('\\', $packageKey, 2);
} else {
$subpackageKey = null;
}
if ($format === null) {
$uriBuilder->setFormat($this->request->getFormat());
} else {
$uriBuilder->setFormat($format);
}

$uri = $uriBuilder->setCreateAbsoluteUri(true)->uriFor($actionName, $arguments, $controllerName, $packageKey, $subpackageKey);
$this->redirectToUri($uri, $delay, $statusCode);
}

/**
* @throws \Neos\Flow\Session\Exception\SessionNotStartedException
*/
public function impersonateUserWithResponseAction(User $user): void
{
/** @var Account $account */
$account = $user->getAccounts()->first();
$this->impersonateService->impersonate($account);
$impersonateStatus = $this->getImpersonateStatus();
$this->view->assign('value', $impersonateStatus);
}

/**
* @throws StopActionException
*/
public function restoreAction(): void
{
$this->impersonateService->restoreOriginalIdentity();
$this->redirectIfPossible('restore');
}


/**
* @throws StopActionException
*/
public function restoreWithResponseAction(): void
{
/** @var Account $originalIdentity */
$originalIdentity = $this->impersonateService->getOriginalIdentity();
/** @var Account $impersonateIdentity */
$impersonateIdentity = $this->impersonateService->getImpersonation();

$response['status'] = false;
if ($originalIdentity) {
$response['status'] = true;
$response['origin'] = [
'accountIdentifier' => $originalIdentity->getAccountIdentifier(),
];
}

if ($impersonateIdentity) {
$response['impersonate'] = [
'accountIdentifier' => $impersonateIdentity->getAccountIdentifier(),
];
}

$this->impersonateService->restoreOriginalIdentity();
$this->view->assign('value', $response);
}

public function statusAction(): void
{
$impersonateStatus = $this->getImpersonateStatus();
$this->view->assign('value', $impersonateStatus);
}

public function getImpersonateStatus(): array
{
$impersonateStatus = [
'status' => false
];

if ($this->impersonateService->isActive()) {
$currentImpersonation = $this->impersonateService->getImpersonation();
$originalIdentity = $this->impersonateService->getOriginalIdentity();
/** @var User $user */
$user = $this->partyService->getAssignedPartyOfAccount($currentImpersonation);

$impersonateStatus['status'] = true;
$impersonateStatus['user'] = [
'accountIdentifier' => $currentImpersonation->getAccountIdentifier(),
'fullName' => $user->getName()->getFullName()
];

if ($originalIdentity) {
/** @var User $originUser */
$originUser = $this->partyService->getAssignedPartyOfAccount($originalIdentity);
$impersonateStatus['origin'] = [
'accountIdentifier' => $originalIdentity->getAccountIdentifier(),
'fullName' => $originUser->getName()->getFullName()
];
}
}

return $impersonateStatus;
}
}
69 changes: 69 additions & 0 deletions Neos.Neos/Classes/Security/ImpersonateAspect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);

namespace Neos\Neos\Security;

/*
* This file is part of the Neos.Neos package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
use Neos\Flow\Security\Authentication\AuthenticationManagerInterface;
use Neos\Neos\Service\ImpersonateService;

/**
* An aspect which centralizes the logging of security relevant actions.
*
* @Flow\Scope("singleton")
* @Flow\Aspect
*/
class ImpersonateAspect
{
/**
* @var bool
*/
protected bool $alreadyLoggedAuthenticateCall = false;

/**
* @var ImpersonateService
* @Flow\Inject
*/
protected $impersonateService;

/**
* @Flow\After("within(Neos\Flow\Security\Authentication\AuthenticationManagerInterface) && method(.*->authenticate())")
* @param JoinPointInterface $joinPoint The current joinpoint
* @return void
* @throws \Exception
*/
public function logManagerAuthenticate(JoinPointInterface $joinPoint): void
{
/** @var AuthenticationManagerInterface $proxy */
$proxy = $joinPoint->getProxy();

if ($this->alreadyLoggedAuthenticateCall === true) {
$this->alreadyLoggedAuthenticateCall = true;
return;
}
if ($proxy->getSecurityContext()->getAccount() === null) {
$this->alreadyLoggedAuthenticateCall = true;
return;
}

if ($this->impersonateService && $this->impersonateService->isActive()) {
$impersonation = $this->impersonateService->getImpersonation();
foreach ($proxy->getSecurityContext()->getAuthenticationTokens() as $token) {
$token->setAccount($impersonation);
}
}

$this->alreadyLoggedAuthenticateCall = true;
}
}

0 comments on commit ed81472

Please sign in to comment.