Passed
Push — master ( 55d5d2...562140 )
by Yaroslav
08:11
created

SensioExtraProvider::createExpression()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4

Importance

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