-
-
Notifications
You must be signed in to change notification settings - Fork 218
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3648 from neos/feature/impersonation
FEATURE: Implement user impersonation
- Loading branch information
Showing
17 changed files
with
830 additions
and
5 deletions.
There are no files selected for viewing
202 changes: 202 additions & 0 deletions
202
Neos.Neos/Classes/Controller/Backend/ImpersonateController.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.