Merge branch 'release/3.0.0'
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful

This commit is contained in:
Ferdinand Kuhl 2022-08-30 14:46:10 +02:00
commit edf3c15935
10 changed files with 218 additions and 20 deletions

View file

@ -0,0 +1,8 @@
pipeline:
code-style:
image: composer
commands:
- composer global config repositories.repo-name vcs https://git.digital-competence.de/Packages/php-codesniffer
- composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
- composer global require digicomp/php-codesniffer:@dev
- composer global exec -- phpcs --runtime-set ignore_warnings_on_exit 1 --standard=DigiComp Classes/ Tests/

View file

@ -0,0 +1,25 @@
workspace:
base: /woodpecker
path: package
matrix:
FLOW_VERSION:
- 7.3
pipeline:
functional-tests:
image: thecodingmachine/php:7.4-v4-cli
environment:
# Enable the PDO_SQLITE extension
- "PHP_EXTENSION_PDO_SQLITE=1"
- "FLOW_VERSION=${FLOW_VERSION}"
- "NEOS_BUILD_DIR=/woodpecker/Build-${FLOW_VERSION}"
commands:
- "sudo mkdir $NEOS_BUILD_DIR"
- "sudo chown -R docker:docker $NEOS_BUILD_DIR"
- "cd $NEOS_BUILD_DIR"
- "composer create-project --no-install neos/flow-base-distribution:^$FLOW_VERSION ."
- "composer config repositories.repo-name path /woodpecker/package"
- "composer config --no-plugins allow-plugins.neos/composer-plugin true"
- "composer require digicomp/flowsessionlock:@dev"
- "bin/phpunit --configuration Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/DigiComp.FlowSessionLock/Tests/Functional"

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Aspects; namespace DigiComp\FlowSessionLock\Aspects;
use DigiComp\FlowSessionLock\Http\SessionLockRequestComponent; use DigiComp\FlowSessionLock\Http\SessionLockRequestMiddleware;
use Neos\Flow\Annotations as Flow; use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface; use Neos\Flow\Aop\JoinPointInterface;
use Neos\Flow\Core\Bootstrap; use Neos\Flow\Core\Bootstrap;
@ -52,9 +52,8 @@ class ReadOnlyAspect
$this->readOnly = true; $this->readOnly = true;
/** @var Lock|null $lock */ /** @var Lock|null $lock */
$lock = $activeRequestHandler->getComponentContext()->getParameter( $lock = $activeRequestHandler->getHttpRequest()->getAttribute(
SessionLockRequestComponent::class, SessionLockRequestMiddleware::class . '.' . SessionLockRequestMiddleware::PARAMETER_NAME
SessionLockRequestComponent::PARAMETER_NAME
); );
if ($lock !== null) { if ($lock !== null) {
$this->logger->debug('SessionLock: Release, as this is marked read only.'); $this->logger->debug('SessionLock: Release, as this is marked read only.');

View file

@ -5,14 +5,16 @@ declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Http; namespace DigiComp\FlowSessionLock\Http;
use Neos\Flow\Annotations as Flow; use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\Component\ComponentContext; use Psr\Http\Message\ResponseInterface;
use Neos\Flow\Http\Component\ComponentInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\Exception\LockAcquiringException; use Symfony\Component\Lock\Exception\LockAcquiringException;
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 SessionLockRequestMiddleware implements MiddlewareInterface
{ {
public const PARAMETER_NAME = 'sessionLock'; public const PARAMETER_NAME = 'sessionLock';
@ -55,13 +57,13 @@ class SessionLockRequestComponent implements ComponentInterface
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function handle(ComponentContext $componentContext): void public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$sessionCookieName = $this->sessionSettings['name']; $sessionCookieName = $this->sessionSettings['name'];
$cookies = $componentContext->getHttpRequest()->getCookieParams(); $cookies = $request->getCookieParams();
if (!isset($cookies[$sessionCookieName])) { if (!isset($cookies[$sessionCookieName])) {
return; return $handler->handle($request);
} }
// TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier // TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier
@ -69,7 +71,7 @@ class SessionLockRequestComponent implements ComponentInterface
$lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease); $lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease);
$componentContext->setParameter(SessionLockRequestComponent::class, static::PARAMETER_NAME, $lock); $request = $request->withAttribute(SessionLockRequestMiddleware::class . '.' . static::PARAMETER_NAME, $lock);
$this->logger->debug('SessionLock: Try to get "' . $key . '"'); $this->logger->debug('SessionLock: Try to get "' . $key . '"');
$timedOut = \time() + $this->secondsToWait; $timedOut = \time() + $this->secondsToWait;
@ -83,5 +85,6 @@ class SessionLockRequestComponent implements ComponentInterface
\usleep(100000); \usleep(100000);
} }
$this->logger->debug('SessionLock: Acquired "' . $key . '"'); $this->logger->debug('SessionLock: Acquired "' . $key . '"');
return $handler->handle($request);
} }
} }

View file

@ -9,9 +9,7 @@ DigiComp:
Neos: Neos:
Flow: Flow:
http: http:
chain: middlewares:
preprocess: lockSession:
chain: position: "before session"
lockSession: middleware: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestMiddleware"
position: "before getSessionCookieFromRequest"
component: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestComponent"

View file

@ -1,3 +1,5 @@
DigiComp: DigiComp:
FlowSessionLock: FlowSessionLock:
lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Testing/SessionLocks/" lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Testing/SessionLocks/"
readOnlyExpressions:
TestUnprotected: "method(DigiComp\\FlowSessionLock\\Tests\\Functional\\Fixtures\\Controller\\ExampleController->unprotectedByConfigurationAction())"

View file

@ -1,6 +1,8 @@
DigiComp.FlowSessionLock DigiComp.FlowSessionLock
------------------------ ------------------------
![Build status](https://ci.digital-competence.de/api/badges/Packages/DigiComp.FlowSessionLock/status.svg)
By default, the session established by Flow is not "protected" in any way. This package restricts every request to load By default, the session established by Flow is not "protected" in any way. This package restricts every request to load
the session only, if there are no other requests having it in access currently. It allows to set custom pointcut which the session only, if there are no other requests having it in access currently. It allows to set custom pointcut which
will set the session in "ReadOnly" mode, which allows concurrent requests to read, but disallows the current request to will set the session in "ReadOnly" mode, which allows concurrent requests to read, but disallows the current request to

View file

@ -0,0 +1,43 @@
<?php
namespace DigiComp\FlowSessionLock\Tests\Functional\Fixtures\Controller;
use DigiComp\FlowSessionLock\Annotations as FlowSessionLock;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
class ExampleController extends ActionController
{
public const CONTROLLER_TIME = 200;
/**
* @Flow\Session(autoStart=true);
* @return string
*/
public function protectedAction()
{
\usleep(static::CONTROLLER_TIME * 1000);
return 'Hello World!';
}
/**
* @Flow\Session(autoStart=true);
* @FlowSessionLock\ReadOnly
* @return string
*/
public function unprotectedByAnnotationAction()
{
\usleep(static::CONTROLLER_TIME * 1000);
return 'Hello World!';
}
/**
* @Flow\Session(autoStart=true);
* @return string
*/
public function unprotectedByConfigurationAction()
{
\usleep(static::CONTROLLER_TIME * 1000);
return 'Hello World!';
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace DigiComp\FlowSessionLock\Tests\Functional;
use DigiComp\FlowSessionLock\Tests\Functional\Fixtures\Controller\ExampleController;
use GuzzleHttp\Psr7\Uri;
use Neos\Flow\Http\Cookie;
use Neos\Flow\Mvc\Routing\Route;
use Neos\Flow\Tests\FunctionalTestCase;
use Psr\Http\Message\ServerRequestFactoryInterface;
class SessionLockRequestComponentTest extends FunctionalTestCase
{
protected ServerRequestFactoryInterface $serverRequestFactory;
protected function setUp(): void
{
parent::setUp();
$this->serverRequestFactory = $this->objectManager->get(ServerRequestFactoryInterface::class);
$route = new Route();
$route->setName('Functional Test - SessionRequestComponent::Restricted');
$route->setUriPattern('test/sessionlock/{@action}');
$route->setDefaults([
'@package' => 'DigiComp.FlowSessionLock',
'@subpackage' => 'Tests\Functional\Fixtures',
'@controller' => 'Example',
'@action' => 'protected',
'@format' => 'html',
]);
$route->setAppendExceedingArguments(true);
$this->router->addRoute($route);
}
public function expectedDuration(): array
{
$parallelChecker = function ($allRequests, $oneRequest) {
self::assertGreaterThan(ExampleController::CONTROLLER_TIME, $oneRequest * 1000);
self::assertLessThan(ExampleController::CONTROLLER_TIME * 4, $allRequests * 1000);
};
return [
[
'http://localhost/test/sessionlock/protected',
function ($allRequests, $oneRequest) {
self::assertGreaterThan(ExampleController::CONTROLLER_TIME, $oneRequest * 1000);
self::assertGreaterThan(ExampleController::CONTROLLER_TIME * 4, $allRequests * 1000);
},
],
[
'http://localhost/test/sessionlock/unprotectedbyannotation',
$parallelChecker,
],
[
'http://localhost/test/sessionlock/unprotectedbyconfiguration',
$parallelChecker,
],
];
}
/**
* @dataProvider expectedDuration
* @test
*/
public function itDoesNotAllowToEnterMoreThanOneWithTheSameSession(string $url, \Closure $checker): void
{
// Functional tests are currently broken, until a version containing
// https://github.com/neos/flow-development-collection/commit/bebfc4e6566bc4ba2ba28330344105adb2d6ada0
// gets released
$request = $this->serverRequestFactory
->createServerRequest('GET', new Uri($url));
$start = \microtime(true);
$response = $this->browser->sendRequest($request);
$neededForOne = \microtime(true) - $start;
$sessionCookies = \array_map(static function ($cookie) {
return Cookie::createFromRawSetCookieHeader($cookie);
}, $response->getHeader('Set-Cookie'));
self::assertNotEmpty($sessionCookies);
$cookies = \array_reduce($sessionCookies, static function ($out, $cookie) {
$out[$cookie->getName()] = $cookie->getValue();
return $out;
}, []);
$nextRequest = $this->serverRequestFactory
->createServerRequest('GET', new Uri($url))
->withCookieParams($cookies);
$childs = [];
$start = \microtime(true);
for ($i = 0; $i < 4; $i++) {
$child = \pcntl_fork();
if ($child === 0) {
$this->browser->sendRequest($nextRequest);
exit();
}
$childs[] = $child;
}
foreach ($childs as $child) {
\pcntl_waitpid($child, $status);
}
$neededForAll = \microtime(true) - $start;
$checker($neededForAll, $neededForOne);
}
}

View file

@ -3,10 +3,13 @@
"description": "Session locking for Neos Flow - it secures the session becoming corrupted by concurrent access to the same session by different requests", "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",
"require": { "require": {
"neos/flow": "^6.3.0", "neos/flow": "^7.3.0",
"php": ">=7.4", "php": ">=7.4",
"symfony/lock": "^5.2.0" "symfony/lock": "^5.2.0"
}, },
"require-dev": {
"ext-pcntl": "*"
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"DigiComp\\FlowSessionLock\\": "Classes/" "DigiComp\\FlowSessionLock\\": "Classes/"
@ -17,8 +20,20 @@
"package-key": "DigiComp.FlowSessionLock" "package-key": "DigiComp.FlowSessionLock"
}, },
"branch-alias": { "branch-alias": {
"dev-develop": "2.0.x-dev" "dev-develop": "3.0.x-dev",
} "dev-version/2.x-dev": "2.1.x-dev"
},
"applied-flow-migrations": [
"Neos.SwiftMailer-20161130105617",
"Neos.Flow-20180415105700",
"Neos.Flow-20190425144900",
"Neos.Flow-20190515215000",
"Neos.Flow-20200813181400",
"Neos.Flow-20201003165200",
"Neos.Flow-20201109224100",
"Neos.Flow-20201205172733",
"Neos.Flow-20201207104500"
]
}, },
"authors": [ "authors": [
{ {