-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGate.php
More file actions
132 lines (115 loc) · 3.8 KB
/
Copy pathGate.php
File metadata and controls
132 lines (115 loc) · 3.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/**
* Italix Auth - Gate
*
* @package Italix\Auth
*/
declare(strict_types=1);
namespace Italix\Auth;
/**
* Asks the policies and decides.
*
* if (!$gate->allows('post.edit', $who, $post)) {
* return $this->redirect_to($this->index_url($lang));
* }
*
* ## The two rules that make the result predictable
*
* **Deny wins.** A policy returning false ends the question; no later policy
* can grant what an earlier one refused. Without that, adding a permissive
* policy silently widens every existing restriction.
*
* **Silence is denial.** If every policy abstains, the answer is *no*. An
* unrecognised action must not be permitted merely because nobody thought to
* forbid it — that is how a typo in an action name becomes an open door.
*
* `explain()` returns which policy decided, because "why can this user not edit
* this record?" is a question that gets asked at three in the afternoon by
* someone who cannot reproduce it.
*/
final class Gate
{
/** @var Policy[] */
private array $policies;
/**
* @param Policy[] $policies consulted in order; the first non-null wins
*/
public function __construct(array $policies = [])
{
foreach ($policies as $policy) {
if (!$policy instanceof Policy) {
throw new AuthException(
'A gate takes ' . Policy::class . ' instances; got '
. (is_object($policy) ? get_class($policy) : gettype($policy)) . '.'
);
}
}
$this->policies = array_values($policies);
}
public function with(Policy $policy): self
{
return new self(array_merge($this->policies, [$policy]));
}
/**
* @param mixed|null $subject
*/
public function allows(string $action_c, ?Identity $who, $subject = null): bool
{
if ($action_c === '') {
throw new AuthException('An empty action can never be decided; name it.');
}
// No identity is not "abstain": it is nobody, and nobody may act.
if ($who === null) {
return false;
}
foreach ($this->policies as $policy) {
$verdict = $policy->decide($action_c, $who, $subject);
if ($verdict !== null) {
return $verdict;
}
}
return false;
}
/**
* @param mixed|null $subject
*/
public function denies(string $action_c, ?Identity $who, $subject = null): bool
{
return !$this->allows($action_c, $who, $subject);
}
/**
* Who decided, and what. For the support ticket.
*
* @param mixed|null $subject
* @return array{allowed: bool, decided_by: string, reason_c: string}
*/
public function explain(string $action_c, ?Identity $who, $subject = null): array
{
if ($who === null) {
return ['allowed' => false, 'decided_by' => '', 'reason_c' => 'no_identity'];
}
foreach ($this->policies as $policy) {
$verdict = $policy->decide($action_c, $who, $subject);
if ($verdict !== null) {
return [
'allowed' => $verdict,
'decided_by' => get_class($policy),
'reason_c' => $verdict ? 'allowed' : 'denied',
];
}
}
return ['allowed' => false, 'decided_by' => '', 'reason_c' => 'no_policy_answered'];
}
/**
* @return string[] registered policy class names, in consultation order
*/
public function policies(): array
{
return array_map('get_class', $this->policies);
}
}