Completed
Branch master (c0d8ef)
by Yaroslav
06:36
created

processIsGrantedAnnotation()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 12
rs 10
c 0
b 0
f 0
ccs 6
cts 6
cp 1
cc 4
nc 2
nop 2
crap 4
1
<?php
2
3
/*
4
 *
5
 * (c) Yaroslav Honcharuk <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Yarhon\RouteGuardBundle\Security\TestProvider;
12
13
use Psr\Log\LoggerAwareTrait;
14
use Symfony\Component\Routing\Route;
15
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
16
use Symfony\Component\ExpressionLanguage\SyntaxError;
17
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security as SecurityAnnotation;
18
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted as IsGrantedAnnotation;
19
use Yarhon\RouteGuardBundle\Annotations\ClassMethodAnnotationReaderInterface;
20
use Yarhon\RouteGuardBundle\Controller\ControllerMetadata;
21
use Yarhon\RouteGuardBundle\Routing\RequestAttributesFactory;
22
use Yarhon\RouteGuardBundle\Routing\RouteMetadataFactory;
23
use Yarhon\RouteGuardBundle\ExpressionLanguage\ExpressionDecorator;
24
use Yarhon\RouteGuardBundle\Security\Test\TestBag;
25
use Yarhon\RouteGuardBundle\Security\Test\SymfonySecurityTest;
26
use Yarhon\RouteGuardBundle\Security\Authorization\SensioSecurityExpressionVoter;
27
use Yarhon\RouteGuardBundle\Exception\LogicException;
28
use Yarhon\RouteGuardBundle\Exception\InvalidArgumentException;
29
30
/**
31
 * SensioSecurityProvider processes Security & IsGranted annotations of Sensio FrameworkExtraBundle.
32
 *
33
 * @see https://symfony.com/doc/5.0/bundles/SensioFrameworkExtraBundle/annotations/security.html
34
 *
35
 * @author Yaroslav Honcharuk <[email protected]>
36
 */
37
class SensioSecurityProvider implements TestProviderInterface
38
{
39
    use LoggerAwareTrait;
40
41
    /**
42
     * @var ClassMethodAnnotationReaderInterface
43
     */
44
    private $annotationReader;
45
46
    /**
47
     * @var ExpressionLanguage
48
     */
49
    private $expressionLanguage;
50
51
    /**
52
     * @var RequestAttributesFactory
53
     */
54
    private $requestAttributesFactory;
55
56
    /**
57
     * @var RouteMetadataFactory
58
     */
59
    private $routeMetadataFactory;
60
61
    /**
62
     * @var array
63
     */
64
    private $tests = [];
65
66
    /**
67
     * @param ClassMethodAnnotationReaderInterface $annotationReader
68
     * @param RequestAttributesFactory             $requestAttributesFactory
69
     * @param RouteMetadataFactory                 $routeMetadataFactory
70
     */
71 18
    public function __construct(ClassMethodAnnotationReaderInterface $annotationReader, RequestAttributesFactory $requestAttributesFactory, RouteMetadataFactory $routeMetadataFactory)
72
    {
73 18
        $this->annotationReader = $annotationReader;
74 18
        $this->requestAttributesFactory = $requestAttributesFactory;
75 18
        $this->routeMetadataFactory = $routeMetadataFactory;
76 18
    }
77
78
    /**
79
     * @param ExpressionLanguage $expressionLanguage
80
     */
81 10
    public function setExpressionLanguage(ExpressionLanguage $expressionLanguage)
82
    {
83 10
        $this->expressionLanguage = $expressionLanguage;
84 10
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 18
    public function getTests($routeName, Route $route, ControllerMetadata $controllerMetadata = null)
90
    {
91 18
        if (!$controllerMetadata) {
92 1
            return null;
93
        }
94
95 17
        $annotations = $this->annotationReader->read($controllerMetadata->getClass(), $controllerMetadata->getMethod(),
96 17
            [SecurityAnnotation::class, IsGrantedAnnotation::class]
97
        );
98
99 17
        if (!count($annotations)) {
100 2
            return null;
101
        }
102
103 16
        $controllerArguments = array_keys($controllerMetadata->getArguments());
104 16
        $requestAttributes = $this->getRequestAttributeNames($route);
105 16
        $requestAttributes = array_diff($requestAttributes, $controllerArguments);
106 16
        $allowedVariables = array_merge($controllerArguments, $requestAttributes);
107
108 16
        $tests = [];
109
110 16
        foreach ($annotations as $annotation) {
111 16
            $subjectName = null;
112
113 16
            if ($annotation instanceof SecurityAnnotation) {
114 8
                $expression = $this->processSecurityAnnotation($annotation, $allowedVariables);
115 6
                $attributes = [$expression];
116 6
                $usedVariables = $expression->getVariableNames();
117 9
            } elseif ($annotation instanceof IsGrantedAnnotation) {
118 9
                list($role, $subjectName) = $this->processIsGrantedAnnotation($annotation, $allowedVariables);
119 8
                $attributes = [$role];
120 8
                $usedVariables = $subjectName ? [$subjectName] : [];
121
            }
122
123 13
            if (count($usedVariables)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $usedVariables does not seem to be defined for all execution paths leading up to this point.
Loading history...
124 7
                $test = new SymfonySecurityTest($attributes, $subjectName);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $attributes does not seem to be defined for all execution paths leading up to this point.
Loading history...
125
126 7
                $usedRequestAttributes = array_values(array_intersect($usedVariables, $requestAttributes));
127
128 7
                if (count($usedRequestAttributes)) {
129 7
                    $test->setMetadata('request_attributes', $usedRequestAttributes);
130
                }
131
            } else {
132 8
                $uniqueKey = $this->getTestAttributesUniqueKey($attributes);
133
134 8
                if (!isset($this->tests[$uniqueKey])) {
135 8
                    $this->tests[$uniqueKey] = new SymfonySecurityTest($attributes);
136
                }
137
138 8
                $test = $this->tests[$uniqueKey];
139
            }
140
141 13
            $tests[] = $test;
142
        }
143
144 13
        return new TestBag($tests);
145
    }
146
147
    /**
148
     * @param SecurityAnnotation $annotation
149
     * @param array              $allowedVariables
150
     *
151
     * @return ExpressionDecorator
152
     */
153 8
    private function processSecurityAnnotation(SecurityAnnotation $annotation, array $allowedVariables)
154
    {
155 8
        if (!$this->expressionLanguage) {
156 1
            throw new LogicException('Cannot create expression because ExpressionLanguage is not provided.');
157
        }
158
159 7
        $expression = $annotation->getExpression();
160
161
        try {
162
            // At first try to create expression without any variable names to save time during expression resolving
163 7
            $expression = $this->createExpression($expression);
164 4
        } catch (InvalidArgumentException $e) {
165 4
            $expression = $this->createExpression($expression, $allowedVariables);
166
        }
167
168 6
        return $expression;
169
    }
170
171
    /**
172
     * @param IsGrantedAnnotation $annotation
173
     * @param array               $allowedVariables
174
     *
175
     * @return array
176
     */
177 9
    private function processIsGrantedAnnotation(IsGrantedAnnotation $annotation, array $allowedVariables)
178
    {
179
        // Despite of the name, $annotation->getAttributes() is a string (annotation value)
180 9
        $role = $annotation->getAttributes();
181
182 9
        $subjectName = $annotation->getSubject() ?: null;
183
184 9
        if (null !== $subjectName && !in_array($subjectName, $allowedVariables, true)) {
185 1
            throw new InvalidArgumentException(sprintf('Unknown subject variable "%s". Allowed variables: "%s".', $subjectName, implode('", "', $allowedVariables)));
186
        }
187
188 8
        return [$role, $subjectName];
189
    }
190
191
    /**
192
     * @param Route $route
193
     *
194
     * @return string[]
195
     */
196 16
    private function getRequestAttributeNames(Route $route)
197
    {
198 16
        $routeMetadata = $this->routeMetadataFactory->createMetadata($route);
199
200 16
        return $this->requestAttributesFactory->getAttributeNames($routeMetadata);
201
    }
202
203
    /**
204
     * @param string $expression
205
     * @param array  $variableNames
206
     *
207
     * @return ExpressionDecorator
208
     *
209
     * @throws InvalidArgumentException
210
     */
211 7
    private function createExpression($expression, array $variableNames = [])
212
    {
213 7
        $voterVariableNames = SensioSecurityExpressionVoter::getVariableNames();
214 7
        $namesToParse = array_merge($voterVariableNames, $variableNames);
215
216
        // TODO: warning if some variable names overlaps with SensioSecurityExpressionVoter variables
217
218
        try {
219 7
            $parsed = $this->expressionLanguage->parse($expression, $namesToParse);
220 4
        } catch (SyntaxError $e) {
221 4
            throw new InvalidArgumentException(sprintf('Cannot parse expression "%s" with following variables: "%s".', $expression, implode('", "', $namesToParse)), 0, $e);
222
        }
223
224 6
        return new ExpressionDecorator($parsed, $variableNames);
225
    }
226
227
    /**
228
     * @param array $attributes
229
     *
230
     * @return string
231
     */
232 8
    private function getTestAttributesUniqueKey(array $attributes)
233
    {
234 8
        $roles = $attributes;
235
236
        $expressions = array_filter($attributes, function ($attribute) {
237 8
            return $attribute instanceof ExpressionDecorator;
238 8
        });
239
240 8
        $roles = array_diff($roles, $expressions);
241
242
        $expressions = array_map(function ($expression) {
243 4
            return (string) $expression;
244 8
        }, $expressions);
245
246 8
        $roles = array_unique($roles);
247 8
        sort($roles);
248
249 8
        return implode('#', array_merge($roles, $expressions));
250
    }
251
}
252