Kde ideálně ověřit oprávnění uživatele na operaci
- 2bfree
- Člen | 248
Na internetu jsem našel několik článků, které se zabývají autentizací a autorizací (např: https://doc.nette.org/…thentication) a několik služeb, které tuto problematiku řeší.
Nikde jsem však nenašel, kam ideálně umístit samotnou kontrolu „… má uživatel dostatečná oprávnění, například pro přístup k souboru či pro provedení nějaké akce “.
Donedávna jsem k této problematice přistupoval „uživatelsky“ a tuto kontrolu jsem umístil do SecuredPresenteru, který v konstruktoru ověřil, že je uživatel přihlášený a že má oprávnění pro přístup k vykreslovanému presenteru a akci a de-facto jsem si schopen tento přístup před sebou i obhájit.
Z druhé strany je zde PHP Guru MVC břitva
Pro lepší představu, co všechno patří do modelu, co by měl model zajišťovat a jak by se měl chovat, pomáhá představit si, že ponecháte stále stejný model, ale kompletně vyměníte controller a view …
Na svůj přístup jsem narazil v momentě, kdy chci svůj model zpřístupnit skrze API. Disponuji ApiPresenterem kde se mapuje požadavek na příslušnou službu a její metodu, takže není problém zde přidat kontrolu, že má uživatel na tuto operaci oprávnění, ale …
Kladu si otázku, je-li požadavek „Může si uživatel změnit heslo?“ (přes frontend aplikace) rozdílný od požadavku „Může si uživatel změnit heslo?“ (přes API) a tedy je výše popsaný přístup korektní, nebo je to totéž a mělo by tedy být ověření oprávnění sjednoceno a tedy právě v Modelu?
A když by mělo být v modelu, jak by to vypadalo? Přeci nebude mít každá třída modelu závislost na identitě, aby si při každém volání public metody ověřovala, že na to má uživatel právo?
A jak to řešíte vy?
Editoval 2bfree (12. 6. 2014 13:09)
- Jan Tvrdík
- Nette guru | 2595
Ověřovat oprávnění v presenteru je v pořádku. Přístup přes API může oprávnění kontrolovat úplně jiným způsobem (např. pomocí API klíčů). Nebo ho nemusí kontrolovat vůbec, protože API bude dostupné jen v rámci interní firemní sítě.
- 2bfree
- Člen | 248
Zvažoval jsem, že uživatele API nějakým způsobem (třeba oAuth 2.0) ověřím a namapuji na Nette Identitu a pak už dále budu ověřovat, že má uživatel právo na akci.
Jen nevím, je-li akce „Chci změnit uživatelovo heslo“ (přes API)
rozdílný od varianty (přes frontend) neboli
„UserListPresenter:changeUserPassword“ od
„ApiPresenter:Users:ChangeUserPassword“ nebo je to totéž a měl bych to
oprávnění mít pojmenované jinak a řešit ho jinak než mít
v constructoru $user->isAllowed(‚jmeno tridy‘, ‚jmeno akce‘).
Každopádně díky za odpověď
Editoval 2bfree (12. 6. 2014 13:37)
- nanuqcz
- Člen | 822
Jan Tvrdík napsal(a):
Ověřovat oprávnění v presenteru je v pořádku. Přístup přes API může oprávnění kontrolovat úplně jiným způsobem (např. pomocí API klíčů).
Což by podle mě měla zapouzdřovat třída Authenticator
. Ta
provede přihlášení (ať už pomocí údajů jméno/heslo, nebo pomocí API
klíčů) a vrátí identitu uživatele. Kterou dostane presenter vždy stejnou,
ať už jde o webové rozhraní, CLI, nějaké API apod.
Podle mě by tedy mělo být ověřování oprávnění někde v mezivrstvě mezi presentery a repository, která je ale součástí modelu. I když se přiznám, sám to tak taky nedělám ;-)
- nanuqcz
- Člen | 822
Nikdy jsem to do hloubky nepromýšlel, ale představuju si to nějak takto:
- Mezi presentery a repository bude existovat vrstva, říkejme jí třeba „service“.
- Bude existovat služba „authorizator“, která dokáže rozhodovat o tom, jestli daný uživatel může provést danou operaci s daným záznamem.
- Když presenter bude něco potřebovat (např. upravit článek blogu), řekne si o to servise (např. articleService). Ta pomocí authorizatoru zkontroluje oprávnění a pomocí articleRepository upraví obsah článku.
Presenter by pak vypadal nějak takto:
<?php
namespace AdminModule;
class ArticlePresenter extends BasePresenter
{
...
public function updateArticleFormSubmitted(Form $form)
{
$this->articleService->update($this->getParam('id'), $form->values);
$this->flashMessage('Article succesfully updated.');
$this->redirect('this');
}
...
}
ArticleService se postará o kontrolu oprávnění (a případně vyhodí
vyjímku), může kontrolovat i další věci (jako např jestli článek
s daným ID existuje), a postará se o zavolání správných repository pro
uložení nových dat do DB.
Myslím, že stejnou myšlenku měl i Jan Tichý ve svém článku Pět vrstev
modelu.
Ale jak říkám, sám toto nepoužívám, protože se mi to všechno nechce psát.
- lucass
- Člen | 89
Zdravím,
k mezivrstvě: osobně to řeším podobně, akorát jí
místo service říkám facade, jak to psal
duke. Je to i z toho důvodu, abych měl tenký presenter a
obslužné metody zejména pro formuláře měly pár řádků. Při
Form::handleSubmit()
tedy volám injectnuté facade, nad tím
příslušnou metodu, které jen předám hodnoty z formuláře (v této fázi
neřeším, jestli jde o create nebo update) a nechám facade, aby dle hodnot,
které jí přišly, rozhodla, co s tím provede, tj. jestli vytvoří nový
záznam, aktualizuje existující atp. Taková metoda vrací pak jen true či
false, dle toho vypíšu flashmessage. Chybu loguji na fasádě, ve které mám
try-catch
pro PDOException
(pokud pracuji s DB),
transakce atd. Stejně řeším i získávání dat – vše jde přes facade
a presenter je lehký, vzdušný a veškerá „zvěrstva“ deleguje na
fasádu.
Fasáda pracuje s více repository, min. však s jedním hlavním. Architekturu dané fasády, tj. jaké repository a jaký typ loggeru (file, DB, mail…) používá, definuji ve speciálním neon konfiguráku, např. takto:
facade.warehouse:
class: WarehouseModule\Facade(@repository.warehouse)
setup:
- addRepository('address', @repository.address)
- addRepository('movement', @repository.warehouseStockMovement)
- addRepository('racking', @repository.warehouseRacking)
- setLogger(@facadeFileLogger)
Každá fasáda má tedy jedno hlavní repository, a 0 až n dalších, pokud jsou potřeba. Pro jeden modul mám jednu fasádu. Pokud fasáda bobtná, rozdrolím modul na submoduly s vlastními fasádami. Někdy se stane, že jeden presenter používá více fasád, např. profile a user (user je pro mě jen zaregistrovaný uživatel s mailem a heslem, profile je v podstatě advanced user, kde je jméno, příjmení a různé další properties).
K oprávnění: standardní operace nad nějakým zdrojem
či objektem jsou CRUD (create
, read
,
update
, delete
), event. samozřejmě i další pro
konkrétní použití, např. vote
, comment
(i když
to jde většinou převést na zdroj a operace nechat dle CRUD) etc., a mohou se
vyskytovat v různých vrstvách aplikace. Můj aktuální přístup: pro
zobrazení různých HTML sekcí na stránce, zejména widgetů a controls,
používám operaci read
, např.
$user->isAllowed('user.group.list', 'read')
, pro zobrazení
tlačítek pro vytváření, úpravu či mazání potom create
,
update
, delete
v kombinaci s oprávněním pro
přístup na konkrétní stránku, např. User:Setup:edit
v podobně $user->isAllowed('User:Setup:, 'edit')
.
Z tohoto plyne, že jsem nějak došel k závěru, že oprávnění jsou
dvojího typu: pro přístup na jednotlivé stránky (řešení automaticky v
BasePresenteru
a metodě startup
) a pak oprávnění
pro operace nad konkrétními widgety/zdroji/komponentami. S čím ale bojuji
je prošpikovanost aplikace v různých místech těmi if-podmínkami. Snažím
se to nějak automatizovat, ale asi to nebude úplně tak jednoduché. V praxi
to znamená, že if-podmínky existují ve view, přímo v HTML, kde vypínám
a zapínám konkrétní HTML části, na presenteru je to if-podmínka pro
přístup na konkrétní stránku (action), v komponentách další
if-podmínky (příp. to řeším zvenčí v presenteru, pokud to komponenta
umožňuje, např.
$grid->getColumn('bar')->setShow($this->getUser()->isAllowed('grid.bar', 'read')
)
a teď si ještě zahrávám s tím, jestli mít další podmínky na fasádě
např. při vytváření nového záznamu. Fasádě bych injectnul uživatele a
opět na příslušná místa doplnil požadované if-podmínky. Nicméně se mi
to už nelíbí.
Řešil jste někdo něco podobného, příp. jak jste se s tím poprali?
Editoval lucass (12. 7. 2014 11:27)