revised code

This commit is contained in:
Robin Krahnen 2022-03-15 09:32:42 +01:00
parent 759b50d2b0
commit 3b3566bb73
10 changed files with 135 additions and 136 deletions

View file

@ -4,7 +4,7 @@ namespace DigiComp\FlowSessionLock\Annotations;
/** /**
* @Annotation * @Annotation
* @Target("METHOD") * @Target({"METHOD"})
*/ */
final class ReadOnly final class ReadOnly
{ {

View file

@ -36,37 +36,43 @@ class ReadOnlyAspect
/** /**
* @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\ReadOnly) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)") * @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\ReadOnly) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)")
* @param JoinPointInterface $joinPoint * @param JoinPointInterface $joinPoint
* * @return mixed
* @return void
*/ */
public function demoteLockToReadOnly(JoinPointInterface $joinPoint) public function demoteLockToReadOnly(JoinPointInterface $joinPoint)
{ {
$handler = $this->bootstrap->getActiveRequestHandler(); $activeRequestHandler = $this->bootstrap->getActiveRequestHandler();
if (! $handler instanceof HttpRequestHandlerInterface) { if (!$activeRequestHandler instanceof HttpRequestHandlerInterface) {
$this->logger->debug(\get_class($handler)); $this->logger->debug('SessionLock: ' . \get_class($activeRequestHandler));
return $joinPoint->getAdviceChain()->proceed($joinPoint); return $joinPoint->getAdviceChain()->proceed($joinPoint);
} }
$componentContext = $handler->getComponentContext();
/** @var Lock $lock */
$lock = $componentContext->getParameter(SessionLockRequestComponent::class, 'sessionLock');
$this->readOnly = true; $this->readOnly = true;
if ($lock) {
$this->logger->debug('SessionLock: Release, as this is marked read only'); /** @var Lock|null $lock */
$lock = $activeRequestHandler->getComponentContext()->getParameter(
SessionLockRequestComponent::class,
SessionLockRequestComponent::PARAMETER_NAME
);
if ($lock !== null) {
$this->logger->debug('SessionLock: Release, as this is marked read only.');
$lock->release(); $lock->release();
} }
return $joinPoint->getAdviceChain()->proceed($joinPoint); return $joinPoint->getAdviceChain()->proceed($joinPoint);
} }
/** /**
* @Flow\Around("method(Neos\Flow\Session\Session->shutdownObject())") * @Flow\Around("method(Neos\Flow\Session\Session->shutdownObject())")
*
* @param JoinPointInterface $joinPoint * @param JoinPointInterface $joinPoint
* @return mixed|void
*/ */
public function doNotSaveSession(JoinPointInterface $joinPoint) public function doNotSaveSession(JoinPointInterface $joinPoint)
{ {
if ($this->readOnly) { if ($this->readOnly) {
return; return;
} }
$joinPoint->getAdviceChain()->proceed($joinPoint);
return $joinPoint->getAdviceChain()->proceed($joinPoint);
} }
} }

View file

@ -4,9 +4,12 @@ namespace DigiComp\FlowSessionLock\Aspects;
use Neos\Flow\Annotations as Flow; use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\Builder\ClassNameIndex; use Neos\Flow\Aop\Builder\ClassNameIndex;
use Neos\Flow\Aop\Exception as NeosFlowAopException;
use Neos\Flow\Aop\Exception\InvalidPointcutExpressionException;
use Neos\Flow\Aop\Pointcut\PointcutFilterComposite; use Neos\Flow\Aop\Pointcut\PointcutFilterComposite;
use Neos\Flow\Aop\Pointcut\PointcutFilterInterface; use Neos\Flow\Aop\Pointcut\PointcutFilterInterface;
use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionParser; use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionParser;
/** /**
@ -15,20 +18,32 @@ use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionPars
*/ */
class ReadOnlyFilter implements PointcutFilterInterface class ReadOnlyFilter implements PointcutFilterInterface
{ {
/**
* @var ConfigurationManager
*/
protected ConfigurationManager $configurationManager; protected ConfigurationManager $configurationManager;
/**
* @var MethodTargetExpressionParser
*/
protected MethodTargetExpressionParser $methodTargetExpressionParser; protected MethodTargetExpressionParser $methodTargetExpressionParser;
/** /**
* @var PointcutFilterComposite[] * @var PointcutFilterComposite[]
*/ */
protected ?array $filters = null; protected ?array $pointcutFilterComposites = null;
/**
* @param ConfigurationManager $configurationManager
*/
public function injectConfigurationManager(ConfigurationManager $configurationManager): void public function injectConfigurationManager(ConfigurationManager $configurationManager): void
{ {
$this->configurationManager = $configurationManager; $this->configurationManager = $configurationManager;
} }
/**
* @param MethodTargetExpressionParser $methodTargetExpressionParser
*/
public function injectMethodTargetExpressionParser(MethodTargetExpressionParser $methodTargetExpressionParser): void public function injectMethodTargetExpressionParser(MethodTargetExpressionParser $methodTargetExpressionParser): void
{ {
$this->methodTargetExpressionParser = $methodTargetExpressionParser; $this->methodTargetExpressionParser = $methodTargetExpressionParser;
@ -36,30 +51,28 @@ class ReadOnlyFilter implements PointcutFilterInterface
/** /**
* @inheritDoc * @inheritDoc
* @throws InvalidConfigurationTypeException
* @throws InvalidPointcutExpressionException
* @throws NeosFlowAopException
*/ */
public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool
{ {
if ($this->filters === null) { $this->buildPointcutFilters();
$this->buildPointcutFilters();
}
$matchingFilters = \array_filter( foreach ($this->pointcutFilterComposites as $pointcutFilterComposite) {
$this->filters, if (
function (PointcutFilterInterface $filter) use ( $pointcutFilterComposite->matches(
$className, $className,
$methodName, $methodName,
$methodDeclaringClassName, $methodDeclaringClassName,
$pointcutQueryIdentifier $pointcutQueryIdentifier
): bool { )
return $filter->matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier); ) {
return true;
} }
);
if ($matchingFilters === []) {
return false;
} }
return true; return false;
} }
/** /**
@ -80,33 +93,47 @@ class ReadOnlyFilter implements PointcutFilterInterface
/** /**
* @inheritDoc * @inheritDoc
* @throws InvalidConfigurationTypeException
* @throws InvalidPointcutExpressionException
* @throws NeosFlowAopException
*/ */
public function reduceTargetClassNames(ClassNameIndex $classNameIndex): ClassNameIndex public function reduceTargetClassNames(ClassNameIndex $classNameIndex): ClassNameIndex
{ {
if ($this->filters === null) { $this->buildPointcutFilters();
$this->buildPointcutFilters();
}
$result = new ClassNameIndex(); $result = new ClassNameIndex();
foreach ($this->filters as $filter) {
$result->applyUnion($filter->reduceTargetClassNames($classNameIndex)); foreach ($this->pointcutFilterComposites as $pointcutFilterComposite) {
$result->applyUnion($pointcutFilterComposite->reduceTargetClassNames($classNameIndex));
} }
return $result; return $result;
} }
/**
* @throws InvalidConfigurationTypeException
* @throws InvalidPointcutExpressionException
* @throws NeosFlowAopException
*/
protected function buildPointcutFilters(): void protected function buildPointcutFilters(): void
{ {
$this->filters = []; if ($this->pointcutFilterComposites !== null) {
$readOnlyExpressions = $this->configurationManager->getConfiguration( return;
ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, }
'DigiComp.FlowSessionLock.readOnlyExpressions'
) ?? []; $this->pointcutFilterComposites = [];
foreach ($readOnlyExpressions as $key => $pointcut) { foreach (
if ($pointcut === null) { $this->configurationManager->getConfiguration(
ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
'DigiComp.FlowSessionLock.readOnlyExpressions'
) as $key => $pointcutExpression
) {
if ($pointcutExpression === null) {
continue; continue;
} }
$this->filters[] = $this->methodTargetExpressionParser->parse(
$pointcut, $this->pointcutFilterComposites[] = $this->methodTargetExpressionParser->parse(
$pointcutExpression,
'Settings.yaml at "DigiComp.FlowSessionLock.readOnlyExpressions", key: "' . $key . '"' 'Settings.yaml at "DigiComp.FlowSessionLock.readOnlyExpressions", key: "' . $key . '"'
); );
} }

View file

@ -5,25 +5,19 @@ namespace DigiComp\FlowSessionLock\Http;
use Neos\Flow\Annotations as Flow; use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\Component\ComponentContext; use Neos\Flow\Http\Component\ComponentContext;
use Neos\Flow\Http\Component\ComponentInterface; use Neos\Flow\Http\Component\ComponentInterface;
use Neos\Flow\Utility\Environment;
use Neos\Utility\Files;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\Key; use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockFactory;
class SessionLockRequestComponent implements ComponentInterface class SessionLockRequestComponent implements ComponentInterface
{ {
/** public const PARAMETER_NAME = 'sessionLock';
* @Flow\InjectConfiguration(package="Neos.Flow", path="session")
* @var array
*/
protected $sessionSettings;
/** /**
* @Flow\Inject(lazy=false) * @Flow\Inject(lazy=false)
* @var LoggerInterface * @var LoggerInterface
*/ */
protected $logger; protected LoggerInterface $logger;
/** /**
* @Flow\Inject(name="DigiComp.FlowSessionLock:LockFactory") * @Flow\Inject(name="DigiComp.FlowSessionLock:LockFactory")
@ -31,40 +25,42 @@ class SessionLockRequestComponent implements ComponentInterface
*/ */
protected $lockFactory; protected $lockFactory;
/**
* @Flow\InjectConfiguration(package="Neos.Flow", path="session")
* @var array
*/
protected array $sessionSettings;
/**
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="timeToLive")
* @var float
*/
protected float $timeToLive;
/** /**
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="autoRelease") * @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="autoRelease")
* @var bool * @var bool
*/ */
protected bool $autoRelease; protected bool $autoRelease;
/**
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="timeToLive")
* @var int
*/
protected int $timeToLive;
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function handle(ComponentContext $componentContext) public function handle(ComponentContext $componentContext)
{ {
$sessionCookieName = $this->sessionSettings['name']; $sessionCookieName = $this->sessionSettings['name'];
$request = $componentContext->getHttpRequest();
$cookies = $request->getCookieParams();
$cookies = $componentContext->getHttpRequest()->getCookieParams();
if (!isset($cookies[$sessionCookieName])) { if (!isset($cookies[$sessionCookieName])) {
return; return;
} }
$sessionIdentifier = $cookies[$sessionCookieName]; // TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier
$key = new Key('session-' . $cookies[$sessionCookieName]);
$key = new Key(
'session-' . $sessionIdentifier
); //TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier
$lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease); $lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease);
$componentContext->setParameter(SessionLockRequestComponent::class, 'sessionLock', $lock); $componentContext->setParameter(SessionLockRequestComponent::class, static::PARAMETER_NAME, $lock);
$this->logger->debug('SessionLock: Get ' . $key); $this->logger->debug('SessionLock: Get ' . $key);
$lock->acquire(true); $lock->acquire(true);

View file

@ -1,31 +0,0 @@
<?php
namespace DigiComp\FlowSessionLock;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Package\Package as BasePackage;
use Neos\Utility\Files;
class Package extends BasePackage
{
public function boot(Bootstrap $bootstrap)
{
parent::boot($bootstrap);
$dispatcher = $bootstrap->getSignalSlotDispatcher();
$dispatcher->connect(
ConfigurationManager::class,
'configurationManagerReady',
function (ConfigurationManager $configurationManager) {
$lockStoreDir = $configurationManager->getConfiguration(
ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
'DigiComp.FlowSessionLock.lockStoreDir'
);
if (is_string($lockStoreDir)) {
Files::createDirectoryRecursively($lockStoreDir);
}
}
);
}
}

View file

@ -1,3 +1,3 @@
DigiComp: DigiComp:
FlowSessionLock: FlowSessionLock:
lockStoreDir: '%FLOW_PATH_DATA%Temporary/Development/SessionLocks' lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Development/SessionLocks/"

View file

@ -1,12 +1,10 @@
DigiComp.FlowSessionLock:LockFactory: DigiComp.FlowSessionLock:LockFactory:
className: 'Symfony\Component\Lock\LockFactory' className: "Symfony\\Component\\Lock\\LockFactory"
arguments: arguments:
1: 1:
object: 'DigiComp.FlowSessionLock:LockStore' object:
factoryObjectName: "Symfony\\Component\\Lock\\Store\\StoreFactory"
DigiComp.FlowSessionLock:LockStore: factoryMethodName: "createStore"
className: 'Symfony\Component\Lock\Store\FlockStore' arguments:
scope: 'singleton' 1:
arguments: setting: "DigiComp.FlowSessionLock.lockStoreConnection"
1:
setting: 'DigiComp.FlowSessionLock.lockStoreDir'

View file

@ -1,3 +1,10 @@
DigiComp:
FlowSessionLock:
lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Production/SessionLocks/"
timeToLive: 300.0
autoRelease: true
readOnlyExpressions: {}
Neos: Neos:
Flow: Flow:
http: http:
@ -5,12 +12,5 @@ Neos:
preprocess: preprocess:
chain: chain:
lockSession: lockSession:
position: 'before getSessionCookieFromRequest' position: "before getSessionCookieFromRequest"
component: 'DigiComp\FlowSessionLock\Http\SessionLockRequestComponent' component: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestComponent"
DigiComp:
FlowSessionLock:
lockStoreDir: '%FLOW_PATH_DATA%Temporary/Production/SessionLocks'
readOnlyExpressions: []
autoRelease: true
timeToLive: 300

View file

@ -0,0 +1,3 @@
DigiComp:
FlowSessionLock:
lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Testing/SessionLocks/"

View file

@ -1,25 +1,11 @@
{ {
"name": "digicomp/flowsessionlock", "name": "digicomp/flowsessionlock",
"description": "Session locking for Neos Flow - it secures the session becoming corrupted by concurrent access to the same session by different requests",
"type": "neos-package", "type": "neos-package",
"description": "Sesion locking for Neos Flow - it secures the session becoming corrupted by concurrent access to the same session by different requests",
"keywords": [
"flow",
"neos"
],
"authors": [
{
"name": "Ferdinand Kuhl",
"email": "f.kuhl@digital-competence.de",
"homepage": "http://www.digital-competence.de",
"role": "Developer"
}
],
"license": "MIT",
"homepage": "https://github.com/digicomp/DigiComp.FlowSessionLock",
"require": { "require": {
"neos/flow": "^6.2", "neos/flow": "^6.2.0",
"php": "^7.4", "php": ">=7.4",
"symfony/lock": "^5.2" "symfony/lock": "^5.2.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -33,5 +19,19 @@
"branch-alias": { "branch-alias": {
"dev-develop": "1.0.x-dev" "dev-develop": "1.0.x-dev"
} }
} },
"authors": [
{
"name": "Ferdinand Kuhl",
"email": "f.kuhl@digital-competence.de",
"homepage": "https://www.digital-competence.de",
"role": "Developer"
}
],
"license": "MIT",
"homepage": "https://github.com/digital-competence/FlowSessionLock",
"keywords": [
"Neos",
"Flow"
]
} }