SymfonyAccessControlProvider::addRule()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 1
nc 1
nop 2
crap 1
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\Expression;
18
use Symfony\Component\ExpressionLanguage\SyntaxError;
19
use Yarhon\RouteGuardBundle\Controller\ControllerMetadata;
20
use Yarhon\RouteGuardBundle\Security\Http\RequestConstraint;
21
use Yarhon\RouteGuardBundle\Security\Http\RouteMatcher;
22
use Yarhon\RouteGuardBundle\Security\Test\TestBag;
23
use Yarhon\RouteGuardBundle\Security\Test\SymfonyAccessControlTest;
24
use Yarhon\RouteGuardBundle\Security\Http\RequestDependentTestBag;
25
use Yarhon\RouteGuardBundle\Security\Authorization\SymfonySecurityExpressionVoter;
26
use Yarhon\RouteGuardBundle\Exception\LogicException;
27
use Yarhon\RouteGuardBundle\Exception\InvalidArgumentException;
28
29
/**
30
 * SymfonyAccessControlProvider processes access_control config of Symfony SecurityBundle.
31
 *
32
 * @see https://symfony.com/doc/4.1/security/access_control.html
33
 *
34
 * @author Yaroslav Honcharuk <[email protected]>
35
 */
36
class SymfonyAccessControlProvider implements ProviderInterface, LoggerAwareInterface
37
{
38
    use LoggerAwareTrait;
39
40
    /**
41
     * @var RouteMatcher
42
     */
43
    private $routeMatcher;
44
45
    /**
46
     * @var array
47
     */
48
    private $rules = [];
49
50
    /**
51
     * @var ExpressionLanguage
52
     */
53
    private $expressionLanguage;
54
55
    /**
56
     * @var array
57
     */
58
    private $tests = [];
59
60
    /**
61
     * @param RouteMatcher $routeMatcher
62
     */
63 22
    public function __construct(RouteMatcher $routeMatcher)
64
    {
65 22
        $this->routeMatcher = $routeMatcher;
66 22
    }
67
68
    /**
69
     * @param ExpressionLanguage $expressionLanguage
70
     */
71 16
    public function setExpressionLanguage(ExpressionLanguage $expressionLanguage)
72
    {
73 16
        $this->expressionLanguage = $expressionLanguage;
74 16
    }
75
76
    /**
77
     * @param array $rules
78
     */
79 18
    public function importRules(array $rules)
80
    {
81 18
        foreach ($rules as $rule) {
82 18
            $transformed = $this->transformRule($rule);
83 16
            $this->addRule(...$transformed);
84
        }
85 16
    }
86
87
    /**
88
     * @param RequestConstraint        $constraint
89
     * @param SymfonyAccessControlTest $test
90
     */
91 19
    public function addRule(RequestConstraint $constraint, SymfonyAccessControlTest $test)
92
    {
93 19
        $this->rules[] = [$constraint, $test];
94 19
    }
95
96
    /**
97
     * @param array $rule
98
     *
99
     * @return array
100
     */
101 18
    private function transformRule(array $rule)
102
    {
103 18
        $constraint = new RequestConstraint($rule['path'], $rule['host'], $rule['methods'], $rule['ips']);
104
105 18
        $attributes = $rule['roles'];
106 18
        if ($rule['allow_if']) {
107 6
            if (!$this->expressionLanguage) {
108 1
                throw new LogicException('Cannot create expression because ExpressionLanguage is not provided.');
109
            }
110
111 5
            $expression = $this->createExpression($rule['allow_if']);
112 4
            $attributes[] = $expression;
113
        }
114
115 16
        $uniqueKey = $this->getTestAttributesUniqueKey($attributes);
116
117 16
        if (!isset($this->tests[$uniqueKey])) {
118 16
            $this->tests[$uniqueKey] = new SymfonyAccessControlTest($attributes);
119
        }
120
121 16
        $test = $this->tests[$uniqueKey];
122
123 16
        return [$constraint, $test];
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 18
    public function getTests($routeName, Route $route, ControllerMetadata $controllerMetadata = null)
130
    {
131 18
        $matches = [];
132
133 18
        foreach ($this->rules as $index => list($constraint, $test)) {
134 17
            $matchResult = $this->routeMatcher->matches($route, $constraint);
135
136 17
            if (false === $matchResult) {
137 15
                continue;
138
            }
139
140 17
            $tests = [$test];
141
142 17
            if (true === $matchResult) {
143 17
                $matches[$index] = [$tests, null];
144 17
                break;
145
            }
146
147 10
            if ($matchResult instanceof RequestConstraint) {
148 10
                $matches[$index] = [$tests, $matchResult];
149 10
                continue;
150
            }
151
        }
152
153 18
        if (!count($matches)) {
154 9
            return null;
155
        }
156
157 17
        $originalMatches = $matches;
158 17
        $matches = array_values($matches);
159
160 17
        if (1 === count($matches) && null === $matches[0][1]) {
161
            // Always matching rule was found, and there were no possibly matching rules found before,
162
            // so we don't need a RequestDependentTestBag for resolving it by RequestContext in runtime.
163 15
            $testBag = new TestBag($matches[0][0]);
164
        } else {
165 10
            $this->logRuntimeMatching($route, $routeName, $originalMatches);
166 10
            $testBag = new RequestDependentTestBag($matches);
167
        }
168
169 17
        return $testBag;
170
    }
171
172
    /**
173
     * @param string $expression
174
     *
175
     * @return Expression
176
     *
177
     * @throws InvalidArgumentException
178
     */
179 5
    private function createExpression($expression)
180
    {
181 5
        $names = SymfonySecurityExpressionVoter::getVariableNames();
182
183
        try {
184 5
            $parsed = $this->expressionLanguage->parse($expression, $names);
185 1
        } catch (SyntaxError $e) {
186 1
            throw new InvalidArgumentException(sprintf('Cannot parse expression "%s" with following variables: "%s".', $expression, implode('", "', $names)), 0, $e);
187
        }
188
189 4
        return $parsed;
190
    }
191
192
    /**
193
     * @param Route  $route
194
     * @param string $routeName
195
     * @param array  $matches
196
     */
197 10
    private function logRuntimeMatching(Route $route, $routeName, array $matches)
198
    {
199 10
        if (!$this->logger) {
200 1
            return;
201
        }
202
203 9
        $message = 'Route "%s" (path "%s") requires runtime matching to access_control rule(s) #%s (zero-based), this would reduce performance.';
204 9
        $this->logger->warning(sprintf($message, $routeName, $route->getPath(), implode(', #', array_keys($matches))));
205 9
    }
206
207
    /**
208
     * @param array $attributes
209
     *
210
     * @return string
211
     */
212 16
    private function getTestAttributesUniqueKey(array $attributes)
213
    {
214 16
        $roles = $attributes;
215
216
        $expressions = array_filter($attributes, function ($attribute) {
217 15
            return $attribute instanceof Expression;
218 16
        });
219
220 16
        $roles = array_diff($roles, $expressions);
221
222
        $expressions = array_map(function ($expression) {
223 4
            return (string) $expression;
224 16
        }, $expressions);
225
226 16
        $roles = array_unique($roles);
227 16
        sort($roles);
228
229 16
        return implode('#', array_merge($roles, $expressions));
230
    }
231
}
232