Groups::__construct()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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