uriBuilder->buildUriFromRoutePath( self::ROUTE_PATH_MODULE, $this->buildUriParametersForClaim($claim, 'module') ); } public function buildVerifyActionUriForClaim(AccessClaim $claim): UriInterface { return $this->uriBuilder->buildUriFromRoutePath( self::ROUTE_PATH_VERIFY, $this->buildUriParametersForClaim($claim, 'verify') ); } /** * Renders the module backend markup, including the `` element. */ public function moduleAction(ServerRequestInterface $request): ResponseInterface { $this->pageRenderer->getJavaScriptRenderer()->addGlobalAssignment([ 'TYPO3' => [ 'configuration' => [ 'username' => htmlspecialchars($this->getBackendUser()->user['username']), ], ], ]); $claim = $this->resolveClaimFromRequest($request, 'module'); if ($claim === null) { return $this->redirectToErrorAction(); } $view = $this->moduleTemplateFactory->create($request); $view->assignMultiple([ 'verifyActionUri' => $this->buildVerifyActionUriForClaim($claim), 'allowInstallToolPassword' => $this->getBackendUser()->isSystemMaintainer(), 'labels' => $this->getLanguageService()->getLabelsFromResource('EXT:backend/Resources/Private/Language/SudoMode.xlf'), ]); return $view->renderResponse('SudoMode/Module'); } /** * Called from JavaScript web-component, throwing an exception that is handled by `SudoModeInterceptor` middleware. */ public function applyAction(ServerRequestInterface $request): ResponseInterface { // @todo security: action is not signed and can be by-passed easily $claim = $this->resolveClaimFromRequest($request, 'apply'); if ($claim === null) { return $this->redirectToErrorAction(); } $this->storage->removeClaim($claim); throw (new RequestGrantedException('Replay request', 1605873757)) ->withInstruction($claim->instruction); } /** * Renders markup with error messages in case `AccessClaim` could not be resolved (e.g. when expired). */ public function errorAction(ServerRequestInterface $request): ResponseInterface { $view = $this->moduleTemplateFactory->create($request); $view->assignMultiple([ 'cancelUri' => $this->backendEntryPointResolver->getPathFromRequest($request), 'cancelTarget' => '_top', 'labels' => $this->getLanguageService()->getLabelsFromResource('EXT:backend/Resources/Private/Language/SudoMode.xlf'), ]); return $view->renderResponse('SudoMode/Error'); } /** * Verifies the provided password, called via AJAX from JavaScript web-component. */ public function verifyAction(ServerRequestInterface $request): ResponseInterface { $claim = $this->resolveClaimFromRequest($request, 'verify'); if ($claim === null) { return new JsonResponse(['message' => 'bad-request'], 400); } $password = (string)($request->getParsedBody()['password'] ?? ''); $useInstallToolPassword = (bool)($request->getParsedBody()['useInstallToolPassword'] ?? false); // Only system maintainers are allowed to use the installtool password for sudo mode operations if (!$this->getBackendUser()->isSystemMaintainer()) { $useInstallToolPassword = false; } $loggerContext = $this->buildLoggerContext($claim); $redirect = [ 'uri' => (string)$this->uriBuilder->buildUriFromRoutePath( self::ROUTE_PATH_APPLY, $this->buildUriParametersForClaim($claim, 'apply') ), ]; $event = $this->eventDispatcher->dispatch(new SudoModeVerifyEvent($claim, $password, $useInstallToolPassword)); if ($event->isVerified()) { $this->logger->info('Passed by PSR-14 SudoModeVerifyEvent', $loggerContext); $this->grantClaim($claim); return new JsonResponse(['message' => 'accessGranted', 'redirect' => $redirect]); } if ($useInstallToolPassword && $this->passwordVerification->verifyInstallToolPassword($password)) { $this->logger->info('Verified with install tool password', $loggerContext); $this->grantClaim($claim); return new JsonResponse(['message' => 'accessGranted', 'redirect' => $redirect]); } if (!$useInstallToolPassword && $this->passwordVerification->verifyBackendUserPassword($password, $this->getBackendUser())) { $this->logger->info('Verified with user password', $loggerContext); $this->grantClaim($claim); return new JsonResponse(['message' => 'accessGranted', 'redirect' => $redirect]); } return new JsonResponse(['message' => 'invalidPassword'], 403); } private function redirectToErrorAction(): ResponseInterface { $uri = $this->uriBuilder->buildUriFromRoutePath(self::ROUTE_PATH_ERROR); return new RedirectResponse($uri); } /** * @param string $additionalPepper used to create a specific signature (e.g. on the action) */ private function buildUriParametersForClaim(AccessClaim $claim, string $additionalPepper): array { $additionalPeppers = [self::class, $additionalPepper]; return [ 'claim' => $claim->id, 'hash' => $this->hashService->hmac($claim->id, json_encode($additionalPeppers), HashAlgo::SHA3_256), ]; } /** * @param string $additionalPepper used to create a specific signature (e.g. on the action) */ private function resolveClaimFromRequest(ServerRequestInterface $request, string $additionalPepper): ?AccessClaim { $claimId = (string)($request->getQueryParams()['claim'] ?? ''); $claimHash = (string)($request->getQueryParams()['hash'] ?? ''); $additionalPeppers = [self::class, $additionalPepper]; $expectedHash = $this->hashService->hmac($claimId, json_encode($additionalPeppers), HashAlgo::SHA3_256); if ($claimId === '' || $claimHash === '' || !hash_equals($expectedHash, $claimHash)) { return null; } return $this->storage->findClaimById($claimId); } private function grantClaim(AccessClaim $claim): void { foreach ($claim->subjects as $subject) { $grant = $this->factory->buildGrantForSubject($subject); $this->storage->addGrant($grant); } } /** * @return array */ private function buildLoggerContext(AccessClaim $claim): array { $backendUserAspect = GeneralUtility::makeInstance(Context::class) ->getAspect('backend.user'); return [ 'claim' => $claim->id, 'user' => $backendUserAspect->get('id'), ]; } private function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } private function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } }