Completed
Push — master ( 77dc19...c43333 )
by Neomerx
01:44
created

DefaultTargetSerializeTrait::evaluateTargets()   D

Complexity

Conditions 9
Paths 12

Size

Total Lines 42
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 42
ccs 21
cts 21
cp 1
rs 4.909
c 0
b 0
f 0
cc 9
eloc 28
nc 12
nop 3
crap 9
1
<?php namespace Limoncello\Auth\Authorization\PolicyDecision\Algorithms;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Generator;
20
use Limoncello\Auth\Contracts\Authorization\PolicyAdministration\TargetInterface;
21
use Limoncello\Auth\Contracts\Authorization\PolicyAdministration\TargetMatchEnum;
22
use Limoncello\Auth\Contracts\Authorization\PolicyInformation\ContextInterface;
23
use Psr\Log\LoggerInterface;
24
use RuntimeException;
25
26
/**
27
 * @package Limoncello\Auth
28
 */
29
trait DefaultTargetSerializeTrait
30
{
31
    /**
32
     * @param ContextInterface     $context
33
     * @param array                $optimizedTargets
34
     * @param LoggerInterface|null $logger
35
     *
36
     * @return Generator
37
     *
38
     * @SuppressWarnings(PHPMD.StaticAccess)
39
     * @SuppressWarnings(PHPMD.ElseExpression)
40
     */
41 26
    public static function evaluateTargets(
42
        ContextInterface $context,
43
        array $optimizedTargets,
44
        ?LoggerInterface $logger
45
    ): Generator {
46 26
        list($isOptimizedForSwitch, $data) = $optimizedTargets;
47 26
        if ($isOptimizedForSwitch === true) {
48 2
            assert(count($data) === 2);
49 2
            list($contextKey, $valueRuleIdMap) = $data;
50 2
            if ($context->has($contextKey) === true &&
51 2
                array_key_exists($targetValue = $context->get($contextKey), $valueRuleIdMap) === true
52
            ) {
53 2
                $matchFound    = true;
54 2
                $matchedRuleId = $valueRuleIdMap[$targetValue];
55
            } else {
56 1
                $matchFound = false;
57
            }
58
59
            // when we are here we already know if the targets has match.
60
            // if match found we firstly yield matched rule ID and then the rest
61
            // otherwise (no match) we just yield 'no match' for every rule ID.
62 2
            if ($matchFound === true) {
63 2
                assert(isset($matchedRuleId));
64 2
                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...
65 1
                foreach ($valueRuleIdMap as $value => $ruleId) {
66 1
                    if ($ruleId !== $matchedRuleId) {
67 1
                        yield TargetMatchEnum::NOT_MATCH => $ruleId;
68
                    }
69
                }
70
            } else {
71 1
                foreach ($valueRuleIdMap as $value => $ruleId) {
72 1
                    yield TargetMatchEnum::NOT_MATCH => $ruleId;
73
                }
74
            }
75
        } else {
76 24
            foreach ($data as $ruleId => $anyOf) {
77 24
                $match = static::evaluateTarget($context, $anyOf, $logger);
78
79 24
                yield $match => $ruleId;
80
            }
81
        }
82
    }
83
84
    /**
85
     * @param ContextInterface     $context
86
     * @param array                $target
87
     * @param LoggerInterface|null $logger
88
     *
89
     * @return int
90
     *
91
     * @SuppressWarnings(PHPMD.StaticAccess)
92
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
93
     * @SuppressWarnings(PHPMD.NPathComplexity)
94
     */
95 26
    protected static function evaluateTarget(
96
        ContextInterface $context,
97
        array $target,
98
        ?LoggerInterface $logger
99
    ): int {
100
        /** @see http://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-os-en.html #7.11 (table 4) */
101
102 26
        assert(Encoder::isTarget($target) === true);
103
104 26
        $anyOfs = Encoder::targetAnyOfs($target);
105 26
        $name   = $logger === null ? null : Encoder::targetName($target);
106
107 26
        if ($anyOfs === null) {
108 16
            $logger === null ?: $logger->info("Target '$name' matches anything.");
109
110 16
            return TargetMatchEnum::NO_TARGET;
111
        }
112
113 24
        $result = TargetMatchEnum::NOT_MATCH;
114 24
        foreach ($anyOfs as $allOfs) {
115
            try {
116 24
                $isAllOfApplicable = true;
117 24
                foreach ($allOfs as $key => $value) {
118 24
                    if ($context->has($key) === false || $context->get($key) !== $value) {
119 18
                        $isAllOfApplicable = false;
120 24
                        break;
121
                    }
122
                }
123 24
                if ($isAllOfApplicable === true) {
124 19
                    $logger === null ?: $logger->info("Target '$name' matches.");
125
126 24
                    return TargetMatchEnum::MATCH;
127
                }
128 1
            } catch (RuntimeException $exception) {
129 1
                $logger === null ?: $logger->warning("Target '$name' got exception from context for its properties.");
130 18
                $result = TargetMatchEnum::INDETERMINATE;
131
            }
132
        }
133
134 17
        $logger === null ?: $logger->debug("Target '$name' has no match.");
135
136 17
        return $result;
137
    }
138
139
    /**
140
     * @param TargetInterface[] $targets
141
     *
142
     * @return array
143
     *
144
     * @SuppressWarnings(PHPMD.ElseExpression)
145
     */
146 36
    protected function optimizeTargets(array $targets): array
147
    {
148 36
        if (($data = $this->tryToEncodeTargetsAsSwitch($targets)) !== null) {
149 3
            $isOptimizedForSwitch = true;
150 3
            assert(count($data) === 2); // context key and value => rule ID pairs.
151
        } else {
152 36
            $isOptimizedForSwitch = false;
153 36
            $data = [];
154 36
            foreach ($targets as $ruleId => $target) {
155 36
                $data[$ruleId] = $this->encodeTarget($target);
156
            }
157
        }
158
159 36
        return [$isOptimizedForSwitch, $data];
160
    }
161
162
    /**
163
     * @param TargetInterface|null $target
164
     *
165
     * @return array
166
     */
167 36
    protected static function encodeTarget(?TargetInterface $target): array
168
    {
169 36
        $name   = null;
170 36
        $anyOfs = null;
171
172 36
        if ($target !== null) {
173 30
            $name = $target->getName();
174 30
            foreach ($target->getAnyOf()->getAllOfs() as $allOf) {
175 30
                $anyOfs[] = $allOf->getPairs();
176
            }
177
        }
178
179
        return [
180 36
            Encoder::TYPE           => Encoder::TYPE_TARGET,
181 36
            Encoder::TARGET_NAME    => $name,
182 36
            Encoder::TARGET_ANY_OFS => $anyOfs,
183
        ];
184
    }
185
186
    /**
187
     * @param TargetInterface[] $targets
188
     *
189
     * @return array|null
190
     *
191
     * @SuppressWarnings(PHPMD.ElseExpression)
192
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
193
     */
194 36
    private function tryToEncodeTargetsAsSwitch(array $targets): ?array
195
    {
196 36
        $result = count($targets) > 1;
197
198 36
        $contextKey  = null;
199 36
        $valueRuleIdMap = [];
200
201 36
        foreach ($targets as $ruleId => $nullableTarget) {
202 36
            if ($result === true &&
203 36
                $nullableTarget !== null &&
204 36
                count($allOfs = $nullableTarget->getAnyOf()->getAllOfs()) === 1 &&
205 36
                count($pairs = reset($allOfs)->getPairs()) === 1
206
            ) {
207 17
                $value = reset($pairs);
208 17
                $key   = key($pairs);
209 17
                assert($key !== null);
210 17
                if ($contextKey === null) {
211 17
                    $contextKey = $key;
212
                }
213 17
                if ($key !== $contextKey) {
214 3
                    $result = false;
215 3
                    break;
216
                }
217 17
                $valueRuleIdMap[$value] = $ruleId;
218
            } else {
219 34
                $result = false;
220 36
                break;
221
            }
222
        }
223
224 36
        $result = $result === true && count($valueRuleIdMap) === count($targets);
225
226
        // if result === true we know the following
227
        // - we have more than one target
228
        // - every target is a really one key, value pair
229
        // - in every such pair key is identical and every value is unique
230
        //
231
        // if so
232
        // $contextKey - will have that identical key and $valueRuleIdMap will be an array of [unique_value => true]
233
        // so later we can check if target matched by simply taking value from context by $contextKey and
234
        // checking that value in $valueRuleIdMap (key existence).
235
236 36
        return $result === true ? [$contextKey, $valueRuleIdMap] : null;
237
    }
238
}
239