Kde ideálně ověřit oprávnění uživatele na operaci

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
2bfree
Člen | 248
+
0
-

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
+
+1
-

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
+
0
-

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
+
0
-

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 ;-)

2bfree
Člen | 248
+
0
-

Neřeším tady problém identifikace uživatele, ten je jasný a krásně vyřešený.

Jak bys implementoval „mezivrstvu“ mezi presentery a repository?

duke
Člen | 650
+
0
-

2bfree napsal:

Jak bys implementoval „mezivrstvu“ mezi presentery a repository?

Nejspíš tak, že do presenteru injektneš servisní třídu a do té servisní třídy injektneš mj. repositáře. Často se těmto třídám říká fasády.

nanuqcz
Člen | 822
+
0
-

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
+
0
-

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)