daikon-cqrs/security-interop

View on GitHub
src/Middleware/SecureActionHandler.php

Summary

Maintainability
A
3 hrs
Test Coverage
F
0%
<?php declare(strict_types=1);
/**
 * This file is part of the daikon-cqrs/security-interop project.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Daikon\Security\Middleware;

use Daikon\Boot\Middleware\Action\ActionInterface;
use Daikon\Boot\Middleware\Action\DaikonRequest;
use Daikon\Boot\Middleware\ActionHandler;
use Daikon\Interop\Assertion;
use Daikon\Interop\AssertionFailedException;
use Daikon\Interop\DaikonException;
use Daikon\Interop\RuntimeException;
use Daikon\Security\Exception\AuthenticationException;
use Daikon\Security\Exception\AuthorizationException;
use Daikon\Security\Middleware\Action\SecureActionInterface;
use Daikon\Validize\Validation\ValidatorDefinition;
use Daikon\Validize\ValueObject\Severity;
use Middlewares\Utils\Factory;
use Psr\Http\Message\ResponseInterface;

final class SecureActionHandler extends ActionHandler
{
    protected function execute(ActionInterface $action, DaikonRequest $request): ResponseInterface
    {
        try {
            // Check action access first before running validation
            if ($action instanceof SecureActionInterface) {
                if (!$action->isAuthorized($request)) {
                    return Factory::createResponse(self::STATUS_FORBIDDEN);
                }
            }

            if ($validator = $action->getValidator($request)) {
                $validatorDefinition = (new ValidatorDefinition('$', Severity::critical()))->withArgument($request);
                $request = $request->withPayload($validator($validatorDefinition));
                Assertion::noContent($request->getErrors());
            }

            // Run secondary resource authorization after validation
            if ($action instanceof SecureActionInterface) {
                if (!$action->isAuthorized($request)) {
                    return Factory::createResponse(self::STATUS_FORBIDDEN);
                }
            }

            $request = $action($request);
        } catch (DaikonException $error) {
            switch (true) {
                case $error instanceof AssertionFailedException:
                    $statusCode = self::STATUS_UNPROCESSABLE_ENTITY;
                    break;
                case $error instanceof AuthenticationException:
                    $statusCode = self::STATUS_UNAUTHORIZED;
                    break;
                case $error instanceof AuthorizationException:
                    $statusCode = self::STATUS_FORBIDDEN;
                    break;
                default:
                    $this->logger->error($error->getMessage(), ['exception' => $error->getTrace()]);
                    $statusCode = self::STATUS_INTERNAL_SERVER_ERROR;
            }
            $request = $action->handleError(
                $request
                    ->withStatusCode($request->getStatusCode($statusCode))
                    ->withErrors($request->getErrors($error))
            );
        }

        if (!$responder = $this->resolveResponder($request)) {
            throw $error ?? new RuntimeException(
                sprintf("Unable to determine responder for '%s'.", get_class($action))
            );
        }

        return $responder->handle($request);
    }
}