Passed
Push — master ( 0d69f0...90ddb2 )
by Daniel
05:49
created

DenyAccessListener::onPreDeserialize()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 18
ccs 0
cts 11
cp 0
crap 30
rs 9.6111
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentsBundle\Security\EventListener;
15
16
use ApiPlatform\Core\Api\IriConverterInterface;
17
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
18
use Silverback\ApiComponentsBundle\Entity\Core\AbstractPageData;
19
use Silverback\ApiComponentsBundle\Repository\Core\AbstractPageDataRepository;
20
use Silverback\ApiComponentsBundle\Repository\Core\RouteRepository;
21
use Silverback\ApiComponentsBundle\Security\Voter\RouteVoter;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpFoundation\Response;
24
use Symfony\Component\HttpKernel\Event\RequestEvent;
25
use Symfony\Component\HttpKernel\HttpKernelInterface;
26
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
27
use Symfony\Component\Security\Core\Security;
28
29
/**
30
 * This will NOT restrict access to components fetched as a collection. As recommended by API Platform best practices, that should
31
 * be implemented in a Doctrine extension by the application developer.
32
 *
33
 * @author Daniel West <[email protected]>
34
 */
35
final class DenyAccessListener
36
{
37
    private RouteRepository $routeRepository;
38
    private AbstractPageDataRepository $pageDataRepository;
39
    private Security $security;
40
    private IriConverterInterface $iriConverter;
41
    private HttpKernelInterface $httpKernel;
42
43
    public function __construct(RouteRepository $routeRepository, AbstractPageDataRepository $pageDataRepository, Security $security, IriConverterInterface $iriConverter, HttpKernelInterface $httpKernel)
44
    {
45
        $this->routeRepository = $routeRepository;
46
        $this->pageDataRepository = $pageDataRepository;
47
        $this->security = $security;
48
        $this->iriConverter = $iriConverter;
49
        $this->httpKernel = $httpKernel;
50
    }
51
52
    public function onPreDeserialize(RequestEvent $event): void
53
    {
54
        $request = $event->getRequest();
55
56
        $resource = $request->attributes->get('data');
57
58
        if ($resource instanceof AbstractComponent) {
59
            if ($this->isComponentAccessible($resource, $request)) {
60
                return;
61
            }
62
            throw new AccessDeniedException('Component access denied.');
63
        }
64
65
        if ($resource instanceof AbstractPageData) {
66
            if (false !== $this->isPageDataAllowedByRoute($resource)) {
67
                return;
68
            }
69
            throw new AccessDeniedException('Page data access denied.');
70
        }
71
    }
72
73
    private function isComponentAccessible(AbstractComponent $component, Request $request): bool
74
    {
75
        // TODO: What if the component is nested in another component's component collection, that's recursive... TBD
76
        // Maybe we need to select the component and traverse up querying as we go... can we .. ?
77
        return true === ($isRouteAllowed = $this->isComponentAllowedByRoute($component)) ||
78
            true === ($isPageDataAllowed = $this->isComponentAllowedByIfPageDataIsReachableAnonymously($component, $request)) ||
79
            (null === $isRouteAllowed && null === $isPageDataAllowed);
80
    }
81
82
    private function isComponentAllowedByIfPageDataIsReachableAnonymously(AbstractComponent $component, Request $request): ?bool
83
    {
84
        $pageDataResources = $this->pageDataRepository->findByComponent($component);
85
86
        // abstain - no results to say yay or nay
87
        if (!\count($pageDataResources)) {
88
            return null;
89
        }
90
91
        foreach ($pageDataResources as $pageDataResource) {
92
            $path = $this->iriConverter->getIriFromItem($pageDataResource);
93
94
            $subRequest = Request::create(
95
                $path,
96
                Request::METHOD_GET,
97
                [],
98
                $request->cookies->all(),
99
                [],
100
                $request->server->all(),
101
                null
102
            );
103
104
            try {
105
                $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
106
107
                return true;
108
            } catch (\Exception $e) {
109
                if (\in_array($e->getCode(), [Response::HTTP_UNAUTHORIZED, Response::HTTP_FORBIDDEN], true)) {
110
                    continue;
111
                }
112
                throw $e;
113
            }
114
        }
115
116
        return false;
117
    }
118
119
    private function isComponentAllowedByRoute(AbstractComponent $component): ?bool
120
    {
121
        $routes = $this->routeRepository->findByComponent($component);
122
123
        if (!\count($routes)) {
124
            return null;
125
        }
126
127
        foreach ($routes as $route) {
128
            if ($this->security->isGranted(RouteVoter::READ_ROUTE, $route)) {
129
                return true;
130
            }
131
        }
132
133
        return false;
134
    }
135
136
    private function isPageDataAllowedByRoute(AbstractPageData $pageData): ?bool
137
    {
138
        $routes = $this->routeRepository->findByPageData($pageData);
139
140
        // abstain - no route to check
141
        if (!\count($routes)) {
142
            return null;
143
        }
144
145
        foreach ($routes as $route) {
146
            $isGranted = $this->security->isGranted(RouteVoter::READ_ROUTE, $route);
147
            if ($isGranted) {
148
                return true;
149
            }
150
        }
151
152
        return false;
153
    }
154
}
155