Issues (62)

Security/TestProvider/SensioExtraProvider.php (2 issues)

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