-
-
Notifications
You must be signed in to change notification settings - Fork 282
/
authorization.texy
295 lines (212 loc) · 11.2 KB
/
authorization.texy
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
Ověřování oprávnění (Autorizace)
********************************
.[perex]
Autorizace zjišťuje, zda má uživatel dostatečná oprávnění například pro přístup k určitému zdroji či pro provedení nějaké akce. Autorizace předpokládá předchozí úspěšnou autentizaci, tj. že uživatel je přihlášen.
→ [Instalace a požadavky |@home#Instalace]
V příkladech budeme používat objekt třídy [api:Nette\Security\User], který představuje aktuálního uživatele a ke kterému se dostanete tak, že si jej necháte předat pomocí [dependency injection |dependency-injection:passing-dependencies]. V presenterech stačí jen zavolat `$user = $this->getUser()`.
.[warning]
Ve verzi 3.0 měly rozhraní ještě prefix `I`, takže názvy byly `Nette\Security\IUserStorage`, `IAuthenticator` a `IAuthorizator` atd. Dále třída `Nette\Security\SimpleIdentity` se jmenovala `Nette\Security\dentity`.
U velmi jednoduchých webů s administrací, kde se nerozlišují oprávnění uživatelů, je možné jako autorizační kritérium použít již známou metodu `isLoggedIn()`. Jinými slovy: jakmile je uživatel přihlášen, má veškerá oprávnění a naopak.
```php
if ($user->isLoggedIn()) { // je uživatel přihlášen?
deleteItem(); // pak má k operaci oprávnění
}
```
Role
----
Smyslem rolí je nabídnout přesnější řízení oprávnění a zůstat nezávislý na uživatelském jméně. Každému uživateli hned při přihlášení přiřkneme jednu či více rolí, ve kterých bude vystupovat. Role mohou být jednoduché řetězce například `admin`, `member`, `guest`, apod. Uvádí se jako druhý parametr konstruktoru `SimpleIdentity`, buď jako řetězec nebo pole řetězců - rolí.
Jako autorizační kritérium nyní použijeme metodu `isInRole()`, která prozradí, zda uživatel vystupuje v dané roli:
```php
if ($user->isInRole('admin')) { // je uživatel v roli admina?
deleteItem(); // pak má k operaci oprávnění
}
```
Jak už víte, po odhlášení uživatele se nemusí smazat jeho identita. Tedy i nadále metoda `getIdentity()` vrací objekt `SimpleIdentity`, včetně všech udělených rolí. Nette Framework vyznává princip „less code, more security“, kdy méně psaní vede k více zabezpečenému kódu, proto při zjišťování rolí nemusíte ještě ověřovat, zda je uživatel přihlášený. Metoda `isInRole()` pracuje s **efektivními rolemi,** tj. pokud je uživatel přihlášen, vychází z rolí uvedených v identitě, pokud přihlášen není, má automaticky speciální roli `guest`.
Autorizátor
-----------
Kromě rolí zavedeme ještě pojmy zdroj a operace:
- **role** je vlastnost uživatele - např. moderátor, redaktor, návštěvník, zaregistrovaný uživatel, správce...
- **zdroj** (*resource*) je nějaký logický prvek webu - článek, stránka, uživatel, položka v menu, anketa, presenter, ...
- **operace** (*operation*) je nějaká konkrétní činnost, kterou uživatel může či nemůže se zdrojem dělat - například smazat, upravit, vytvořit, hlasovat, ...
Autorizátor je objekt, který rozhoduje, zda má daná *role* povolení provést určitou *operaci* s určitým *zdrojem*. Jde o objekt implementující rozhraní [api:Nette\Security\Authorizator] s jedinou metodu `isAllowed()`:
```php
class MyAuthorizator implements Nette\Security\Authorizator
{
public function isAllowed($role, $resource, $operation): bool
{
if ($role === 'admin') {
return true;
}
if ($role === 'user' && $resource === 'article') {
return true;
}
// ...
return false;
}
}
```
Autorizátor přidáme do konfigurace [jako službu|dependency-injection:services] DI kontejneru:
```neon
services:
- MyAuthorizator
```
A následuje příklad použití. Pozor, tentokrát voláme metodu `Nette\Security\User::isAllowed()`, nikoliv autorizátor, takže tam není první parametr `$role`. Tato metoda volá `MyAuthorizator::isAllowed()` postupně pro všechny uživatelovy role a vrací true, pokud alespoň jedna z nich má povolení.
```php
if ($user->isAllowed('file')) { // může uživatel dělat cokoliv se zdrojem 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // může nad zdrojem 'file' provést 'delete'?
deleteFile();
}
```
Oba parametry jsou volitelné, výchozí hodnota `null` má význam *cokoliv*.
Permission ACL
--------------
Nette přichází s vestavěnou implementací autorizátoru, a to třídou [api:Nette\Security\Permission] poskytující programátorovi lehkou a flexibilní ACL (Access Control List) vrstvu pro řízení oprávnění a přístupů. Práce s ní spočívá v definici rolí, zdrojů a jednotlivých oprávnění. Přičemž role a zdroje umožňují vytvářet hierarchie. Na vysvětlenou si ukážeme příklad webové aplikace:
- `guest`: nepřihlášený návštěvník, který může číst a procházet veřejnou část webu, tzn. číst články, komentáře a volit v anketách
- `registered`: přihlášený registrovaný uživatel, který navíc může komentovat
- `admin`: může spravovat články, komentáře i ankety
Nadefinovali jsme si tedy určité role (`guest`, `registered` a `admin`) a zmínili zdroje (`article`, `comment`, `poll`), ke kterým mohou uživatelé s nějakou rolí přistupovat nebo provádět určité operace (`view`, `vote`, `add`, `edit`).
Vytvoříme instanci třídy Permission a nadefinujeme **role**. Lze přitom využít tzv. dědičnost rolí, která zajistí, že např. uživatel s rolí administrátora (`admin`) může dělat i to co obyčejný návštěvník webu (a samozřejmě i více).
```php
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' dědí od 'guest'
$acl->addRole('admin', 'registered'); // a od něj dědí 'admin'
```
Nyní nadefinujeme i seznam **zdrojů**, ke kterým mohou uživatelé přistupovat.
```php
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
```
I zdroje mohou používat dědičnost, bylo by možné například zadat `$acl->addResource('perex', 'article')`.
A teď to nejdůležitější. Nadefinujeme mezi nimi pravidla určující, kdo co může s čím dělat:
```php
// nejprve nikdo nemůže dělat nic
// nechť guest může prohlížet články, komentáře i ankety
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// a v anketách navíc i hlasovat
$acl->allow('guest', 'poll', 'vote');
// registrovaný dědí práva od guesta, dáme mu navíc právo komentovat
$acl->allow('registered', 'comment', 'add');
// administrátor může prohlížet a editovat cokoliv
$acl->allow('admin', $acl::ALL, ['view', 'edit', 'add']);
```
Co když chceme někomu **zamezit** k určitému zdroji přístup?
```php
// administrátor nemůže editovat ankety, to by bylo nedemokratické
$acl->deny('admin', 'poll', 'edit');
```
Nyní, když máme vytvořený seznam pravidel, můžeme jednoduše klást autorizační dotazy:
```php
// může guest prohlížet články?
$acl->isAllowed('guest', 'article', 'view'); // true
// může guest editovat články?
$acl->isAllowed('guest', 'article', 'edit'); // false
// může guest hlasovat v anketách?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// může guest komentovat?
$acl->isAllowed('guest', 'comment', 'add'); // false
```
Totéž platí pro registrovaného uživatele, ten však může i komentovat:
```php
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
```
Administrátor může editovat vše, kromě anket:
```php
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
```
Oprávění mohou také být vyhodnocována dynamicky a můžeme rozhodnutí nechat na vlastním callbacku, kterému se předají všechny parametry:
```php
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return /* ... */;
};
$acl->allow('registered', 'comment', null, $assertion);
```
Jak ale třeba řešit situaci, kdy nestačí jen názvy rolí a zdrojů, ale chtěli bychom definovat, že třeba role `registered` může editovat zdroj `article` jen pokud je jeho autorem? Místo řetězců použijeme objekty, role bude objekt [api:Nette\Security\Role] a zdroj [api:Nette\Security\Resource]. Jejich metody `getRoleId()` resp. `getResourceId()` budou vracet původní řetezce:
```php
class Registered implements Nette\Security\Role
{
public $id;
public function getRoleId(): string
{
return 'registered';
}
}
class Article implements Nette\Security\Resource
{
public $authorId;
public function getResourceId(): string
{
return 'article';
}
}
```
A nyní vytvoříme pravidlo:
```php
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // objekt Registered
$resource = $acl->getQueriedResource(); // objekt Article
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
```
A dotaz na ACL se provede předáním objektů:
```php
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
```
Role může dědit od jiné role či od více rolí. Co se ale stane, pokud má jeden předek akci zakázanou a druhý povolenou? Jaké budou práva potomka? Určuje se to podle váhy role - poslední uvedená role v seznamu předků má největší váhu, první uvedená role tu nejmenší. Více názorné je to z příkladu:
```php
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// případ A: role admin má menší váhu než role guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// případ B: role admin má větší váhu než guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
```
Role a zdroje lze i odebírat (`removeRole()`, `removeResource()`), lze revertovat i pravidla (`removeAllow()`, `removeDeny()`). Pole všech přímých rodičovských rolí vrací `getRoleParents()`, zda od sebe dvě entity dědí vrací `roleInheritsFrom()` a `resourceInheritsFrom()`.
Přidání jako služby
-------------------
Námi vytvořené ACL si potřebujeme předat do konfigurace jako službu, aby jej začal používat objekt `$user`, tedy aby bylo možné používat v kódu např. `$user->isAllowed('article', 'view')`. Za tím účelem si na něj napíšeme továrnu:
```php
namespace App\Model;
class AuthorizatorFactory
{
public static function create(): Nette\Security\Permission
{
$acl = new Nette\Security\Permission;
$acl->addRole(/* ... */);
$acl->addResource(/* ... */);
$acl->allow(/* ... */);
return $acl;
}
}
```
A přidáme ji do konfigurace:
```neon
services:
- App\Model\AuthorizatorFactory::create
```
V presenterech pak můžete ověřit oprávnění například v metodě `startup()`:
```php
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}
```