Completed
Push — master ( 18bc97...e7bdcd )
by Yaroslav
09:10
created

SensioExtraProvider   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 213
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 23
eloc 76
dl 0
loc 213
ccs 75
cts 75
cp 1
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A createExpression() 0 14 2
A processSecurityAnnotation() 0 16 3
A setExpressionLanguage() 0 3 1
A getRequestAttributeNames() 0 5 1
A __construct() 0 5 1
A getTestAttributesUniqueKey() 0 18 1
A processIsGrantedAnnotation() 0 12 4
B getTests() 0 56 10
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\SensioExtraTest;
26
use Yarhon\RouteGuardBundle\Security\Authorization\SensioSecurityExpressionVoter;
27
use Yarhon\RouteGuardBundle\Exception\LogicException;
28
use Yarhon\RouteGuardBundle\Exception\InvalidArgumentException;
29
30
/**
31
 * SensioExtraProvider 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 SensioExtraProvider 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 27
    public function __construct(ClassMethodAnnotationReaderInterface $annotationReader, RequestAttributesFactory $requestAttributesFactory, RouteMetadataFactory $routeMetadataFactory)
72
    {
73 27
        $this->annotationReader = $annotationReader;
74 27
        $this->requestAttributesFactory = $requestAttributesFactory;
75 27
        $this->routeMetadataFactory = $routeMetadataFactory;
76 27
    }
77
78
    /**
79
     * @param ExpressionLanguage $expressionLanguage
80
     */
81 19
    public function setExpressionLanguage(ExpressionLanguage $expressionLanguage)
82
    {
83 19
        $this->expressionLanguage = $expressionLanguage;
84 19
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 27
    public function getTests($routeName, Route $route, ControllerMetadata $controllerMetadata = null)
90
    {
91 27
        if (!$controllerMetadata) {
92 1
            return null;
93
        }
94
95 26
        $annotations = $this->annotationReader->read($controllerMetadata->getClass(), $controllerMetadata->getMethod(),
96 26
            [SecurityAnnotation::class, IsGrantedAnnotation::class]
97
        );
98
99 26
        if (!count($annotations)) {
100 11
            return null;
101
        }
102
103 25
        $controllerArguments = array_keys($controllerMetadata->getArguments());
104 25
        $requestAttributes = $this->getRequestAttributeNames($route);
105 25
        $requestAttributes = array_diff($requestAttributes, $controllerArguments);
106 25
        $allowedVariables = array_merge($controllerArguments, $requestAttributes);
107
108 25
        $tests = [];
109
110 25
        foreach ($annotations as $annotation) {
111 25
            $subjectName = null;
112
113 25
            if ($annotation instanceof SecurityAnnotation) {
114 17
                $expression = $this->processSecurityAnnotation($annotation, $allowedVariables);
115 15
                $attributes = [$expression];
116 15
                $usedVariables = $expression->getVariableNames();
117 18
            } elseif ($annotation instanceof IsGrantedAnnotation) {
118 18
                list($role, $subjectName) = $this->processIsGrantedAnnotation($annotation, $allowedVariables);
119 17
                $attributes = [$role];
120 17
                $usedVariables = $subjectName ? [$subjectName] : [];
121
            }
122
123 22
            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 16
                $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...
125
126 16
                $usedRequestAttributes = array_values(array_intersect($usedVariables, $requestAttributes));
127
128 16
                if (count($usedRequestAttributes)) {
129 16
                    $test->setMetadata('request_attributes', $usedRequestAttributes);
130
                }
131
            } else {
132 17
                $uniqueKey = $this->getTestAttributesUniqueKey($attributes);
133
134 17
                if (!isset($this->tests[$uniqueKey])) {
135 17
                    $this->tests[$uniqueKey] = new SensioExtraTest($attributes);
136
                }
137
138 17
                $test = $this->tests[$uniqueKey];
139
            }
140
141 22
            $tests[] = $test;
142
        }
143
144 22
        return new TestBag($tests);
145
    }
146
147
    /**
148
     * @param SecurityAnnotation $annotation
149
     * @param array              $allowedVariables
150
     *
151
     * @return ExpressionDecorator
152
     */
153 17
    private function processSecurityAnnotation(SecurityAnnotation $annotation, array $allowedVariables)
154
    {
155 17
        if (!$this->expressionLanguage) {
156 1
            throw new LogicException('Cannot create expression because ExpressionLanguage is not provided.');
157
        }
158
159 16
        $expression = $annotation->getExpression();
160
161
        try {
162
            // At first try to create expression without any variable names to save time during expression resolving
163 16
            $expression = $this->createExpression($expression);
164 14
        } catch (InvalidArgumentException $e) {
165 14
            $expression = $this->createExpression($expression, $allowedVariables);
166
        }
167
168 15
        return $expression;
169
    }
170
171
    /**
172
     * @param IsGrantedAnnotation $annotation
173
     * @param array               $allowedVariables
174
     *
175
     * @return array
176
     */
177 18
    private function processIsGrantedAnnotation(IsGrantedAnnotation $annotation, array $allowedVariables)
178
    {
179
        // Despite of the name, $annotation->getAttributes() is a string (annotation value)
180 18
        $role = $annotation->getAttributes();
181
182 18
        $subjectName = $annotation->getSubject() ?: null;
183
184 18
        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 17
        return [$role, $subjectName];
189
    }
190
191
    /**
192
     * @param Route $route
193
     *
194
     * @return string[]
195
     */
196 25
    private function getRequestAttributeNames(Route $route)
197
    {
198 25
        $routeMetadata = $this->routeMetadataFactory->createMetadata($route);
199
200 25
        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 16
    private function createExpression($expression, array $variableNames = [])
212
    {
213 16
        $voterVariableNames = SensioSecurityExpressionVoter::getVariableNames();
214 16
        $namesToParse = array_merge($voterVariableNames, $variableNames);
215
216
        // TODO: warning if some variable names overlaps with SensioSecurityExpressionVoter variables
217
218
        try {
219 16
            $parsed = $this->expressionLanguage->parse($expression, $namesToParse);
220 14
        } catch (SyntaxError $e) {
221 14
            throw new InvalidArgumentException(sprintf('Cannot parse expression "%s" with following variables: "%s".', $expression, implode('", "', $namesToParse)), 0, $e);
222
        }
223
224 15
        return new ExpressionDecorator($parsed, $variableNames);
225
    }
226
227
    /**
228
     * @param array $attributes
229
     *
230
     * @return string
231
     */
232 17
    private function getTestAttributesUniqueKey(array $attributes)
233
    {
234 17
        $roles = $attributes;
235
236
        $expressions = array_filter($attributes, function ($attribute) {
237 17
            return $attribute instanceof ExpressionDecorator;
238 17
        });
239
240 17
        $roles = array_diff($roles, $expressions);
241
242
        $expressions = array_map(function ($expression) {
243 13
            return (string) $expression;
244 17
        }, $expressions);
245
246 17
        $roles = array_unique($roles);
247 17
        sort($roles);
248
249 17
        return implode('#', array_merge($roles, $expressions));
250
    }
251
}
252