Completed
Pull Request — master (#11)
by Arnold
03:10
created

Groups::calcUserRoles()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 15
ccs 11
cts 11
cp 1
rs 9.9332
cc 3
nc 3
nop 0
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jasny\Auth\Authz;
6
7
use Improved\IteratorPipeline\Pipeline;
8
use Jasny\Auth\AuthzInterface as Authz;
9
use Jasny\Auth\ContextInterface as Context;
10
use Jasny\Auth\UserInterface as User;
11
12
/**
13
 * Authorize by access group.
14
 * Can be used for ACL (Access Control List).
15
 * @immutable
16
 *
17
 * <code>
18
 *   $authz = new Authz\Groups([
19
 *     'user' => [],
20
 *     'accountant' => ['user'],
21
 *     'moderator' => ['user'],
22
 *     'developer' => ['user'],
23
 *     'admin' => ['moderator', 'developer']
24
 *   ]);
25
 *
26
 *   $auth = new Auth($authz);
27
 * </code>
28
 */
29
class Groups implements Authz
30
{
31
    use StateTrait;
32
33
    /** @var array<string,string[]> */
34
    protected array $groups;
35
36
    /**
37
     * Current authenticated user
38
     */
39
    protected ?User $user = null;
40
41
    /**
42
     * The authorization context. This could be an organization, where a user has specific roles per organization
43
     * rather than roles globally.
44
     */
45
    protected ?Context $context = null;
46
47
    /**
48
     * Cached user level. Service has an immutable state.
49
     * @var string[]
50
     */
51
    protected array $userRoles = [];
52
53
    /**
54
     * AuthzByGroup constructor.
55
     *
56
     * @param array<string,string[]> $groups
57
     */
58 15
    public function __construct(array $groups)
59
    {
60 15
        foreach ($groups as $group => &$roles) {
61 15
            $roles = $this->expand($group, $groups);
62
        }
63
64 15
        $this->groups = $groups;
65 15
    }
66
67
    /**
68
     * Get a copy of the service with a modified property and recalculated
69
     * Returns $this if authz hasn't changed.
70
     *
71
     * @param string $property
72
     * @param string $value
73
     * @return static
74
     */
75 12
    protected function withProperty(string $property, $value): self
76
    {
77 12
        $clone = clone $this;
78 12
        $clone->{$property} = $value;
79
80 12
        $clone->calcUserRoles();
81
82 12
        $isSame = $clone->{$property} === $this->{$property} && $clone->userRoles === $this->userRoles;
83
84 12
        return $isSame ? $this : $clone;
85
    }
86
87
    /**
88
     * Expand groups to include all roles they supersede.
89
     *
90
     * @param string                 $role
91
     * @param array<string,string[]> $groups
92
     * @param string[]               $expanded  Accumulator
93
     * @return string[]
94
     */
95 15
    protected function expand(string $role, array $groups, array &$expanded = []): array
96
    {
97 15
        $expanded[] = $role;
98
99
        // Ignore duplicates.
100 15
        $additionalRoles = array_diff($groups[$role], $expanded);
101
102
        // Remove current role from groups to prevent issues from cross-references.
103 15
        $groupsWithoutCurrent = array_diff_key($groups, [$role => null]);
104
105
        // Recursively expand the superseded roles.
106 15
        foreach ($additionalRoles as $additionalRole) {
107 15
            $this->expand($additionalRole, $groupsWithoutCurrent, $expanded);
108
        }
109
110 15
        return $expanded;
111
    }
112
113
114
    /**
115
     * Get all available authorization roles (for the current context).
116
     *
117
     * @return string[]
118
     */
119 1
    public function getAvailableRoles(): array
120
    {
121 1
        return array_keys($this->groups);
122
    }
123
124
    /**
125
     * Check if the current user is logged in and has specified role.
126
     */
127 12
    public function is(string $role): bool
128
    {
129 12
        if (!isset($this->groups[$role])) {
130 1
            trigger_error("Unknown authz role '$role'", E_USER_WARNING); // Catch typos
131 1
            return false;
132
        }
133
134 11
        return in_array($role, $this->userRoles, true);
135
    }
136
137
    /**
138
     * Get a copy, recalculating the authz level of the user.
139
     * Returns $this if authz hasn't changed.
140
     *
141
     * @return static
142
     */
143 3
    public function recalc(): self
144
    {
145 3
        $clone = clone $this;
146 3
        $clone->calcUserRoles();
147
148 3
        return $clone->userRoles === $this->userRoles ? $this : $clone;
149
    }
150
151
    /**
152
     * Calculate the (expanded) roles of the current user.
153
     */
154 12
    protected function calcUserRoles(): void
155
    {
156 12
        if ($this->user === null) {
157 1
            $this->userRoles = [];
158 1
            return;
159
        }
160
161 11
        $role = $this->user->getAuthRole($this->context);
162 11
        $roles = is_array($role) ? $role : [$role];
163
164 11
        $this->userRoles = Pipeline::with($roles)
165 11
            ->map(fn($role) => $this->groups[$role] ?? [])
166 11
            ->flatten()
167 11
            ->unique()
168 11
            ->toArray();
169 11
    }
170
}
171