diff --git a/docs/en/middleware.rst b/docs/en/middleware.rst index ff9cd32..735fdd5 100644 --- a/docs/en/middleware.rst +++ b/docs/en/middleware.rst @@ -176,6 +176,26 @@ option:: 'requireAuthorizationCheck' => false ])); +You can also use a callable to conditionally skip the authorization check based +on the request. This is useful when you need to bypass authorization for specific +routes (e.g., plugin admin panels that manage their own authorization):: + + $middlewareQueue->add(new AuthorizationMiddleware($this, [ + 'requireAuthorizationCheck' => function ($request) { + // Skip authorization check for specific routes + $path = $request->getUri()->getPath(); + if (str_contains($path, '/admin/queue')) { + return false; + } + + return true; + } + ])); + +The callable receives the ``ServerRequestInterface`` and should return a boolean. +Return ``true`` to require authorization check (default behavior), or ``false`` +to skip the check for that request. + Handling Unauthorized Requests ------------------------------ diff --git a/src/Middleware/AuthorizationMiddleware.php b/src/Middleware/AuthorizationMiddleware.php index ecdc8de..12d5be8 100644 --- a/src/Middleware/AuthorizationMiddleware.php +++ b/src/Middleware/AuthorizationMiddleware.php @@ -56,7 +56,8 @@ class AuthorizationMiddleware implements MiddlewareInterface * - `requireAuthorizationCheck` When true the middleware will raise an exception * if no authorization checks were done. This aids in ensuring that all actions * check authorization. It is intended as a development aid and not to be relied upon - * in production. Defaults to `true`. + * in production. Defaults to `true`. Can also be a callable that receives the + * ServerRequestInterface and returns a boolean, allowing route-based control. * * @var array */ @@ -135,7 +136,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface try { $response = $handler->handle($request); - if ($this->getConfig('requireAuthorizationCheck') && !$service->authorizationChecked()) { + $requireCheck = $this->getConfig('requireAuthorizationCheck'); + if (is_callable($requireCheck)) { + $requireCheck = $requireCheck($request); + } + if ($requireCheck && !$service->authorizationChecked()) { throw new AuthorizationRequiredException(['url' => $request->getRequestTarget()]); } } catch (Exception $exception) { diff --git a/tests/TestCase/Middleware/AuthorizationMiddlewareTest.php b/tests/TestCase/Middleware/AuthorizationMiddlewareTest.php index c7ffeed..454c366 100644 --- a/tests/TestCase/Middleware/AuthorizationMiddlewareTest.php +++ b/tests/TestCase/Middleware/AuthorizationMiddlewareTest.php @@ -316,4 +316,68 @@ public function testMiddlewareInjectsServiceIntoDICViaCustomContainerInstance() $this->assertEquals($service, $container->get(AuthorizationService::class)); } + + public function testRequireAuthorizationCheckCallableReturnsTrue(): void + { + $this->expectException(AuthorizationRequiredException::class); + + $service = $this->createMock(AuthorizationServiceInterface::class); + $service->expects($this->once()) + ->method('authorizationChecked') + ->willReturn(false); + + $request = (new ServerRequest())->withAttribute('identity', ['id' => 1]); + $handler = new TestRequestHandler(); + + $middleware = new AuthorizationMiddleware($service, [ + 'requireAuthorizationCheck' => function (ServerRequestInterface $request): bool { + return true; + }, + 'identityDecorator' => IdentityDecorator::class, + ]); + $middleware->process($request, $handler); + } + + public function testRequireAuthorizationCheckCallableReturnsFalse(): void + { + $service = $this->createMock(AuthorizationServiceInterface::class); + $service->expects($this->never()) + ->method('authorizationChecked'); + + $request = (new ServerRequest())->withAttribute('identity', ['id' => 1]); + $handler = new TestRequestHandler(); + + $middleware = new AuthorizationMiddleware($service, [ + 'requireAuthorizationCheck' => function (ServerRequestInterface $request): bool { + return false; + }, + 'identityDecorator' => IdentityDecorator::class, + ]); + $result = $middleware->process($request, $handler); + + $this->assertInstanceOf(ResponseInterface::class, $result); + } + + public function testRequireAuthorizationCheckCallableWithRouteBasedLogic(): void + { + $service = $this->createMock(AuthorizationServiceInterface::class); + $service->expects($this->never()) + ->method('authorizationChecked'); + + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/admin/queue']); + $handler = new TestRequestHandler(); + + $middleware = new AuthorizationMiddleware($service, [ + 'requireAuthorizationCheck' => function (ServerRequestInterface $request): bool { + // Skip authorization check for admin/queue routes + $path = $request->getUri()->getPath(); + + return !str_contains($path, '/admin/queue'); + }, + 'identityDecorator' => IdentityDecorator::class, + ]); + $result = $middleware->process($request, $handler); + + $this->assertInstanceOf(ResponseInterface::class, $result); + } }