Skip to content

Commit

Permalink
Add FilePermissionMiddleware
Browse files Browse the repository at this point in the history
  • Loading branch information
lcharette committed Oct 29, 2023
1 parent bd0aebb commit 1da0098
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 0 deletions.
13 changes: 13 additions & 0 deletions app/config/default.php
Original file line number Diff line number Diff line change
Expand Up @@ -424,4 +424,17 @@
'entrypoints' => 'assets://entrypoints.json',
'manifest' => 'assets://manifest.json',
],

/**
* ----------------------------------------------------------------------
* Writable Stream Config
* ----------------------------------------------------------------------
* Resource stream to check for write permission. True means it should be
* writeable, false means it should not be writable.
*/
'writable' => [
'logs://' => true,
'cache://' => true,
'sessions://' => true,
],
];
9 changes: 9 additions & 0 deletions app/config/testing.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,13 @@
'session' => [
'handler' => env('TEST_SESSION_HANDLER', 'array'),
],

/*
* Don't force writable directories in tests.
*/
'writable' => [
'logs://' => null,
'cache://' => null,
'sessions://' => null,
],
];
2 changes: 2 additions & 0 deletions app/src/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
use UserFrosting\Sprinkle\Core\Listeners\ModelInitiated;
use UserFrosting\Sprinkle\Core\Listeners\ResourceLocatorInitiated;
use UserFrosting\Sprinkle\Core\Listeners\SetRouteCaching;
use UserFrosting\Sprinkle\Core\Middlewares\FilePermissionMiddleware;
use UserFrosting\Sprinkle\Core\Middlewares\LocaleMiddleware;
use UserFrosting\Sprinkle\Core\Middlewares\SessionMiddleware;
use UserFrosting\Sprinkle\Core\Middlewares\URIMiddleware;
Expand Down Expand Up @@ -211,6 +212,7 @@ public function getMiddlewares(): array
CsrfGuardMiddleware::class,
SessionMiddleware::class,
URIMiddleware::class,
FilePermissionMiddleware::class,
ExceptionHandlerMiddleware::class,
];
}
Expand Down
65 changes: 65 additions & 0 deletions app/src/Middlewares/FilePermissionMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

/*
* UserFrosting Core Sprinkle (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/sprinkle-core
* @copyright Copyright (c) 2021 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/sprinkle-core/blob/master/LICENSE.md (MIT License)
*/

namespace UserFrosting\Sprinkle\Core\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use UserFrosting\Config\Config;
use UserFrosting\Sprinkle\Core\Exceptions\BadConfigException;
use UserFrosting\UniformResourceLocator\ResourceLocatorInterface;

/**
* Middleware used to check if the file permissions are correct.
*/
class FilePermissionMiddleware implements MiddlewareInterface
{
/**
* Inject dependencies
*
* @param ResourceLocatorInterface $locator
* @param Config $config
*/
public function __construct(
protected ResourceLocatorInterface $locator,
protected Config $config,
) {
}

/**
* {@inheritdoc}
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
foreach ($this->config->get('writable') as $stream => $assertWriteable) {
// Since config can't be removed, we skip if the value is null
if ($assertWriteable === null) {
continue;
}

// Translate stream to file path
$file = $this->locator->findResource($stream);

// Check if file exist and is writeable
if ($file === null || $assertWriteable !== is_writable($file)) {
// If file doesn't exist, we try to find the expected path
$expectedPath = $this->locator->findResource($stream, false, true);

throw new BadConfigException("Stream $stream doesn't exist and is not writeable. Make sure path `$expectedPath` exist and is writeable.");
}
}

return $handler->handle($request);
}
}
155 changes: 155 additions & 0 deletions app/tests/Unit/Middlewares/FilePermissionMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php

declare(strict_types=1);

/*
* UserFrosting Core Sprinkle (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/sprinkle-core
* @copyright Copyright (c) 2021 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/sprinkle-core/blob/master/LICENSE.md (MIT License)
*/

namespace UserFrosting\Sprinkle\Core\Tests\Unit\Middlewares;

use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use phpmock\mockery\PHPMockery;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use ReflectionClass;
use UserFrosting\Config\Config;
use UserFrosting\Sprinkle\Core\Exceptions\BadConfigException;
use UserFrosting\Sprinkle\Core\Middlewares\FilePermissionMiddleware;
use UserFrosting\UniformResourceLocator\ResourceLocatorInterface;

class FilePermissionMiddlewareTest extends TestCase
{
use MockeryPHPUnitIntegration;

public function testWithPathNotFound(): void
{
$config = new Config([
'writable' => [
'foo://' => true,
],
]);

/** @var RequestHandlerInterface */
$handler = Mockery::mock(RequestHandlerInterface::class)
->shouldNotReceive('handle')
->getMock();

/** @var ServerRequestInterface */
$request = Mockery::mock(ServerRequestInterface::class);

/** @var ResourceLocatorInterface */
$locator = Mockery::mock(ResourceLocatorInterface::class)
->shouldReceive('findResource')->once()->with('foo://')->andReturn(null)
->shouldReceive('findResource')->once()->with('foo://', false, true)->andReturn('app/foo')
->getMock();

// Set Expectation
$this->expectException(BadConfigException::class);
$this->expectExceptionMessage("Stream foo:// doesn't exist and is not writeable. Make sure path `app/foo` exist and is writeable.");

$middleware = new FilePermissionMiddleware($locator, $config);
$middleware->process($request, $handler);
}

public function testWithWritable(): void
{
$config = new Config([
'writable' => [
'foo://' => true,
],
]);

// Mock built-in is_writable
$reflection_class = new ReflectionClass(FilePermissionMiddleware::class);
$namespace = $reflection_class->getNamespaceName();
PHPMockery::mock($namespace, 'is_writable')->andReturn(true);

/** @var RequestHandlerInterface */
$handler = Mockery::mock(RequestHandlerInterface::class)
->shouldReceive('handle')
->once()
->with(Mockery::type(ServerRequestInterface::class))
->andReturn(Mockery::mock(ResponseInterface::class))
->getMock();

/** @var ServerRequestInterface */
$request = Mockery::mock(ServerRequestInterface::class);

/** @var ResourceLocatorInterface */
$locator = Mockery::mock(ResourceLocatorInterface::class)
->shouldReceive('findResource')->once()->with('foo://')->andReturn('app/foo')
->getMock();

$middleware = new FilePermissionMiddleware($locator, $config);
$middleware->process($request, $handler);
}

public function testWithNotWritable(): void
{
$config = new Config([
'writable' => [
'foo://' => true,
],
]);

// Mock built-in is_writable
$reflection_class = new ReflectionClass(FilePermissionMiddleware::class);
$namespace = $reflection_class->getNamespaceName();
PHPMockery::mock($namespace, 'is_writable')->andReturn(false);

/** @var RequestHandlerInterface */
$handler = Mockery::mock(RequestHandlerInterface::class)
->shouldNotReceive('handle')
->getMock();

/** @var ServerRequestInterface */
$request = Mockery::mock(ServerRequestInterface::class);

/** @var ResourceLocatorInterface */
$locator = Mockery::mock(ResourceLocatorInterface::class)
->shouldReceive('findResource')->once()->with('foo://')->andReturn('app/foo')
->shouldReceive('findResource')->once()->with('foo://', false, true)->andReturn('app/foo')
->getMock();

// Set Expectation
$this->expectException(BadConfigException::class);
$this->expectExceptionMessage("Stream foo:// doesn't exist and is not writeable. Make sure path `app/foo` exist and is writeable.");

$middleware = new FilePermissionMiddleware($locator, $config);
$middleware->process($request, $handler);
}

public function testWithNotConfig(): void
{
$config = new Config([
'writable' => [
'foo://' => null,
],
]);

/** @var RequestHandlerInterface */
$handler = Mockery::mock(RequestHandlerInterface::class)
->shouldNotReceive('handle')
->once()
->with(Mockery::type(ServerRequestInterface::class))
->andReturn(Mockery::mock(ResponseInterface::class))
->getMock();

/** @var ServerRequestInterface */
$request = Mockery::mock(ServerRequestInterface::class);

/** @var ResourceLocatorInterface */
$locator = Mockery::mock(ResourceLocatorInterface::class);

$middleware = new FilePermissionMiddleware($locator, $config);
$middleware->process($request, $handler);
}
}

0 comments on commit 1da0098

Please sign in to comment.