DefaultTargetSerializeTrait   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 6

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 38
lcom 0
cbo 6
dl 0
loc 210
ccs 84
cts 84
cp 1
rs 9.36
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
B evaluateTargets() 0 42 9
C evaluateTarget() 0 43 13
A optimizeTargets() 0 15 3
A encodeTarget() 0 18 3
B tryToEncodeTargetsAsSwitch() 0 44 10
1
<?php declare(strict_types=1);
2
3
namespace Limoncello\Auth\Authorization\PolicyDecision\Algorithms;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use Generator;
22
use Limoncello\Auth\Contracts\Authorization\PolicyAdministration\TargetInterface;
23
use Limoncello\Auth\Contracts\Authorization\PolicyAdministration\TargetMatchEnum;
24
use Limoncello\Auth\Contracts\Authorization\PolicyInformation\ContextInterface;
25
use Psr\Log\LoggerInterface;
26
use RuntimeException;
27
use function assert;
28
use function array_key_exists;
29
use function count;
30
use function key;
31
use function reset;
32
33
/**
34
 * @package Limoncello\Auth
35
 */
36
trait DefaultTargetSerializeTrait
37
{
38
    /**
39
     * @param ContextInterface     $context
40
     * @param array                $optimizedTargets
41 26
     * @param LoggerInterface|null $logger
42
     *
43
     * @return Generator
44
     *
45
     * @SuppressWarnings(PHPMD.StaticAccess)
46 26
     * @SuppressWarnings(PHPMD.ElseExpression)
47 26
     */
48 2
    public static function evaluateTargets(
49 2
        ContextInterface $context,
50 2
        array $optimizedTargets,
51 2
        ?LoggerInterface $logger
52
    ): Generator {
53 2
        list($isOptimizedForSwitch, $data) = $optimizedTargets;
54 2
        if ($isOptimizedForSwitch === true) {
55
            assert(count($data) === 2);
56 1
            list($contextKey, $valueRuleIdMap) = $data;
57
            if ($context->has($contextKey) === true &&
58
                array_key_exists($targetValue = $context->get($contextKey), $valueRuleIdMap) === true
59
            ) {
60
                $matchFound    = true;
61
                $matchedRuleId = $valueRuleIdMap[$targetValue];
62 2
            } else {
63 2
                $matchFound = false;
64 2
            }
65 1
66 1
            // when we are here we already know if the targets has match.
67 1
            // if match found we firstly yield matched rule ID and then the rest
68
            // otherwise (no match) we just yield 'no match' for every rule ID.
69
            if ($matchFound === true) {
70
                assert(isset($matchedRuleId));
71 1
                yield TargetMatchEnum::MATCH => $matchedRuleId;
0 ignored issues
show
Bug introduced by
The variable $matchedRuleId does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
72 1
                foreach ($valueRuleIdMap as $value => $ruleId) {
73
                    if ($ruleId !== $matchedRuleId) {
74
                        yield TargetMatchEnum::NOT_MATCH => $ruleId;
75
                    }
76 24
                }
77 24
            } else {
78
                foreach ($valueRuleIdMap as $value => $ruleId) {
79 24
                    yield TargetMatchEnum::NOT_MATCH => $ruleId;
80
                }
81
            }
82
        } else {
83
            foreach ($data as $ruleId => $anyOf) {
84
                $match = static::evaluateTarget($context, $anyOf, $logger);
85
86
                yield $match => $ruleId;
87
            }
88
        }
89
    }
90
91
    /**
92
     * @param ContextInterface     $context
93
     * @param array                $target
94
     * @param LoggerInterface|null $logger
95 26
     *
96
     * @return int
97
     *
98
     * @SuppressWarnings(PHPMD.StaticAccess)
99
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
100
     * @SuppressWarnings(PHPMD.NPathComplexity)
101
     */
102 26
    protected static function evaluateTarget(
103
        ContextInterface $context,
104 26
        array $target,
105 26
        ?LoggerInterface $logger
106
    ): int {
107 26
        /** @see http://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-os-en.html #7.11 (table 4) */
108 16
109
        assert(Encoder::isTarget($target) === true);
110 16
111
        $anyOfs = Encoder::targetAnyOfs($target);
112
        $name   = $logger === null ? null : Encoder::targetName($target);
113 24
114 24
        if ($anyOfs === null) {
115
            $logger === null ?: $logger->debug("Target '$name' matches anything.");
116 24
117 24
            return TargetMatchEnum::NO_TARGET;
118 24
        }
119 18
120 24
        $result = TargetMatchEnum::NOT_MATCH;
121
        foreach ($anyOfs as $allOfs) {
122
            try {
123 24
                $isAllOfApplicable = true;
124 19
                foreach ($allOfs as $key => $value) {
125
                    if ($context->has($key) === false || $context->get($key) !== $value) {
126 24
                        $isAllOfApplicable = false;
127
                        break;
128 1
                    }
129 1
                }
130 18
                if ($isAllOfApplicable === true) {
131
                    $logger === null ?: $logger->debug("Target '$name' matches.");
132
133
                    return TargetMatchEnum::MATCH;
134 17
                }
135
            } catch (RuntimeException $exception) {
136 17
                $logger === null ?: $logger->warning("Target '$name' got exception from context for its properties.");
137
                $result = TargetMatchEnum::INDETERMINATE;
138
            }
139
        }
140
141
        $logger === null ?: $logger->debug("Target '$name' has no match.");
142
143
        return $result;
144
    }
145
146 36
    /**
147
     * @param TargetInterface[] $targets
148 36
     *
149 3
     * @return array
150 3
     *
151
     * @SuppressWarnings(PHPMD.ElseExpression)
152 36
     */
153 36
    protected function optimizeTargets(array $targets): array
154 36
    {
155 36
        if (($data = $this->tryToEncodeTargetsAsSwitch($targets)) !== null) {
156
            $isOptimizedForSwitch = true;
157
            assert(count($data) === 2); // context key and value => rule ID pairs.
158
        } else {
159 36
            $isOptimizedForSwitch = false;
160
            $data = [];
161
            foreach ($targets as $ruleId => $target) {
162
                $data[$ruleId] = $this->encodeTarget($target);
163
            }
164
        }
165
166
        return [$isOptimizedForSwitch, $data];
167 36
    }
168
169 36
    /**
170 36
     * @param TargetInterface|null $target
171
     *
172 36
     * @return array
173 30
     */
174 30
    protected static function encodeTarget(?TargetInterface $target): array
175 30
    {
176
        $name   = null;
177
        $anyOfs = null;
178
179
        if ($target !== null) {
180 36
            $name = $target->getName();
181 36
            foreach ($target->getAnyOf()->getAllOfs() as $allOf) {
182 36
                $anyOfs[] = $allOf->getPairs();
183
            }
184
        }
185
186
        return [
187
            Encoder::TYPE           => Encoder::TYPE_TARGET,
188
            Encoder::TARGET_NAME    => $name,
189
            Encoder::TARGET_ANY_OFS => $anyOfs,
190
        ];
191
    }
192
193
    /**
194 36
     * @param TargetInterface[] $targets
195
     *
196 36
     * @return array|null
197
     *
198 36
     * @SuppressWarnings(PHPMD.ElseExpression)
199 36
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
200
     */
201 36
    private function tryToEncodeTargetsAsSwitch(array $targets): ?array
202 36
    {
203 36
        $result = count($targets) > 1;
204 36
205 36
        $contextKey  = null;
206
        $valueRuleIdMap = [];
207 17
208 17
        foreach ($targets as $ruleId => $nullableTarget) {
209 17
            if ($result === true &&
210 17
                $nullableTarget !== null &&
211 17
                count($allOfs = $nullableTarget->getAnyOf()->getAllOfs()) === 1 &&
212
                count($pairs = reset($allOfs)->getPairs()) === 1
213 17
            ) {
214 3
                $value = reset($pairs);
215 3
                $key   = key($pairs);
216
                assert($key !== null);
217 17
                if ($contextKey === null) {
218
                    $contextKey = $key;
219 34
                }
220 36
                if ($key !== $contextKey) {
221
                    $result = false;
222
                    break;
223
                }
224 36
                $valueRuleIdMap[$value] = $ruleId;
225
            } else {
226
                $result = false;
227
                break;
228
            }
229
        }
230
231
        $result = $result === true && count($valueRuleIdMap) === count($targets);
232
233
        // if result === true we know the following
234
        // - we have more than one target
235
        // - every target is a really one key, value pair
236 36
        // - in every such pair key is identical and every value is unique
237
        //
238
        // if so
239
        // $contextKey - will have that identical key and $valueRuleIdMap will be an array of [unique_value => true]
240
        // so later we can check if target matched by simply taking value from context by $contextKey and
241
        // checking that value in $valueRuleIdMap (key existence).
242
243
        return $result === true ? [$contextKey, $valueRuleIdMap] : null;
244
    }
245
}
246