Files
Php-Template/src/Http/Middleware/ScopeMiddleware.php
Ron Rise 4bc6f5251a
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 4m18s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 4m30s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 4m21s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m35s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 4m21s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 3m13s
chore: add deployment configurations and tests for logger and dispatcher
2025-11-14 13:01:02 -05:00

95 lines
3.9 KiB
PHP

<?php
declare(strict_types=1);
namespace Siteworxpro\App\Http\Middleware;
use League\Route\Dispatcher;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Siteworxpro\App\Attributes\Guards\Scope;
use Siteworxpro\App\Controllers\Controller;
use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\HttpStatus\CodesEnum;
/**
* Middleware that enforces scope-based access control on controller actions.
*
* It inspects PHP 8 attributes of type \`Scope\` applied to the resolved controller method,
* compares the required scopes with the user scopes provided on the request attribute \`scopes\`,
* and returns a 403 JSON response when any required scope is missing.
*
* If the route callable cannot be resolved, or no scope is required, the request is passed through.
*
* @see Scope
*/
class ScopeMiddleware extends Middleware
{
/**
* Resolve the route callable, read any \`Scope\` attributes, and enforce required scopes.
*
* Expected user scopes are provided on the request under the attribute name \`scopes\`
* as an array of strings.
*
* @param ServerRequestInterface $request Incoming PSR-7 request (expects \`scopes\` attribute).
* @param RequestHandlerInterface|Dispatcher $handler Next handler or League\Route dispatcher.
*
* @return ResponseInterface A 403 JSON response when scopes are insufficient; otherwise the handler response.
*
* @throws \JsonException If encoding the JSON error response fails.
* @throws \ReflectionException If reflection on the controller or method fails.
*/
public function process(
ServerRequestInterface $request,
RequestHandlerInterface | Dispatcher $handler
): ResponseInterface {
// Attempt to resolve the route's callable [Controller instance, method name].
$callable = $this->extractRouteCallable($handler);
if ($callable === null) {
// If no callable is available, delegate to the next handler.
return $handler->handle($request);
}
/** @var Controller $class Controller instance resolved from the route. */
[$class, $method] = $callable;
// Ensure the controller exists and the method is defined before reflecting.
if (class_exists($class::class)) {
$reflectionClass = new \ReflectionClass($class);
if ($reflectionClass->hasMethod($method)) {
$reflectionMethod = $reflectionClass->getMethod($method);
// Fetch all Scope attributes declared on the method.
$attributes = $reflectionMethod->getAttributes(Scope::class);
foreach ($attributes as $attribute) {
/** @var Scope $scopeInstance Concrete Scope attribute instance. */
$scopeInstance = $attribute->newInstance();
$requiredScopes = $scopeInstance->getScopes();
// Retrieve user scopes from the request (defaults to an empty array).
$userScopes = $request->getAttribute('scopes', []);
// Deny if any required scope is missing from the user's scopes.
if (
array_any(
$requiredScopes,
fn($requiredScope) => !in_array($requiredScope, $userScopes, true)
)
) {
return JsonResponseFactory::createJsonResponse([
'error' => 'insufficient_scope',
'error_description' =>
'The request requires higher privileges than provided by the access token.'
], CodesEnum::FORBIDDEN);
}
}
}
}
// All checks passed; continue down the middleware pipeline.
return $handler->handle($request);
}
}