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

SymfonyAccessControlProvider::transformRule()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

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