Ověřování oprávnění a loginu ve formuláři

fikusir
Člen | 25
+
0
-

Ahoj,

aktuálně ověřuji zda je uživatel přihlášení a má správnou roli v metodě presenteru renderDefault, nicméně šablona používá komponentu formuláře a kliknutí na tlačítko ve formuláři volá metodu XXXPressed, kde znovu ověřuji uživatele oproti přihlášení a roli.

Není to zbytečné nebo je to naopak žádoucí?

Moc díky, Michal

	public function renderDefault(): void
	{

		if (!$this->getUser()->isLoggedIn()){
			$this->flashMessage('Pro vstup do toho modulu musíte být přihlášeni.','alert-warning');
			$this->redirect(':Default:default');
		}

		if (!$this->getUser()->isInRole('logisticinfo') && !$this->getUser()->isInRole('admin')){
			$this->flashMessage('Do tohoto modulu nemáte přístup.','alert-warning');
			$this->redirect(':Default:default');
		}

	protected function createComponentChangedateForm(): UI\Form
	{
		$form = new UI\Form;

		$form->addText('dispatch_date')
			->setHtmlType('date')
			->setRequired("Pole %label je povinné")

		$form->addSubmit('save', 'Přejít')->setHtmlAttribute('class','btn btn-success')->onClick[] = [$this, 'saveDatePressed'];
		return $form;
	}

	public function saveDatePressed($button, $values): void
	{
		if (!$this->getUser()->isLoggedIn()){
			$this->flashMessage('Pro vstup do toho modulu musíte být přihlášeni.','alert-danger');
			$this->redirect(':Default:default');
		}

		if (!$this->getUser()->isInRole('logisticinfo_docs') && !$this->getUser()->isInRole('admin')){
			$this->flashMessage('Pro editaci data je nutná speciální role oprávnění.','alert-warning');
			$this->redirect('Delivery:edit',$values->delivery_id);
		}

		$this->redirect("Delivery:edit", $values->delivery_id);
	}
Marek Bartoš
Nette Blogger | 1155
+
+4
-

Jak při odeslání, tak při výpisu je třeba ověřovat oprávnění. I bez toho, abys mi formulář zobrazil ho mohu odeslat, pokud vím, jaká data ode mne čeká. Stejně tak pokud už má uživatel stránku načtenou a mezi zobrazením formuláře a odesláním mu odebereš oprávnění.

fikusir
Člen | 25
+
0
-

Díky, říkal jsem si, že to asi tak bude. Měj prima den!

Ozzrel
Generous Backer | 51
+
+4
-

Ahoj, myslím že řešit přihlášení/kontrolu až v metodě renderNěco() je už dost pozdě. Užitečnější je startup() nebo actionNěco() tam se to vyřeší rychleji a navíc tam jde i řešit co/kam vlastně dál. Navíc ten startup() zařízne všechno včetně formulářů.

Rick Strafy
Nette Blogger | 65
+
-1
-

@fikusir staci ak to overenie z renderDefault() presunies do actionDetault() alebo startup(), tak to nemusis overovat v tej save metode, cyklus presenteru je startup ⇒ action metody ⇒ spracovanie formov ⇒ render metody ⇒ renderovanie formov, v render metodach sa take veci nemaju robit
//edit: tak uz to niekto napisal predomnou, podstatne je ze action a startup sa vola pred formularmi

Editoval Rick Strafy (8. 9. 2021 14:16)

Kamil Valenta
Člen | 757
+
-1
-

Úplně nejrychlejší to bude v checkRequirements(), dost možná se některé objekty totiž ani nebudou muset instancovat.

Marek Bartoš
Nette Blogger | 1155
+
-1
-

Za předpokladu, že má celý presenter stejné oprávnění je startup() check jednoduší (action, pokud je víc než jedna a oprávněními se liší), ale na stránce přece můžete mít více komponent s různými oprávněními. Bezpečnější je vždy oprávnění řešit přímo v komponentě a případně vyřešit, jak se ve startupu dotázat na oprávnění té komponenty, bez které vykreslení presenteru nedává smysl.

Editoval Marek Bartoš (8. 9. 2021 14:38)

Polki
Člen | 553
+
0
-

Záleží, jak má fikusir aplikaci navrženou, ale většinou to je tak, že bývá podstránka fixnutá na určitou akci.
Tedy jakože je jedna podstránka na přidání něčeho, jedna na editaci něčeho atp.
Podle dnešních trendů máš mít ale na každou takovou akci samotný presenter..
No ovšem někdy se stane, že máš třeba video (co je komponenta jež můžeš zobrazit) a k němu komentáře, které můžeš mazat, editovat, přidávat zobrazovat…

Normálně by pro každou z těchto částí vypadalo řešení oprávnění jinak.

V případě, že máš na každou akci zvlášť presenter by prostě stačilo ve startup metodě zakázat celou práci s Presenterem a jeho komponentami, jak píše Marek Bartoš.

Pokud ale máš v presenteru více akcí, z nichž některé mohou uživatelé vykonávat a jiné ne, tak toto nebude fungovat a musíš vložit ověření do ‚action‘ metody, jak píší Ozzrel a Rick Strafy. Problém zde je, že pokud to ověříš pouze v action metodě, tak jsem schopný z akce do které mám přístup poslat validní request na odeslání formuláře z akce, do které přístup nemám a to proto, že komponenty nejsou vázány na žádnou akci presenteru, ale existují souběžně ve všech akcích, jak píše Marek Bartoš.

Ověřování až v komponentách je ale pozdě. Komponenta se musí vytvořit, nějak na něco napojit atp.. Pokud uživatel nemá oprávnění, tak by se komponenta neměla už ani vytvořit, natož tak po vytvoření sama oprávnění hlídat.

Nejlepší řešení, které jsem zatím viděl vypadalo takto:

class ArticlePresenter extends Presneter
{
	#[Inject] public ArticleModifyFormControlFactory $articleModifyFormControlFactory;
	#[Inject] public ArticleRepository $articleRepository;
	private ?Article $article = null; // Může být i ActiveRow
	private bool $canCreateArticleModifyForm = false;

	public function actionAdd(): void
	{
		if (!$this->user->isAllowed('article', 'add')) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		}
		$this->canCreateArticleModifyForm = true;
	}

	public function actionEdit(int $id): void
	{
		$article = $this->articleRepository->getById($id);
		if (!$article) {
			$this->error('Not found', IResponse::S404_NOT_FOUND);
		}
		if (!$this->user->isAllowed($article, 'edit')) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		}
		$this->canCreateArticleModifyForm = true;
	}

	public function actionDetail(int $id): void
	{
		$article = $this->articleRepository->getById($id);
		if (!$article) {
			$this->error('Not found', IResponse::S404_NOT_FOUND);
		}
		if (!$this->user->isAllowed($article, 'view')) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		}
		$this->template->article = $article;
	}

	public function createComponentArticleModifyForm(): Control
	{
		if (!$this->canCreateArticleModifyForm) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		}
		return $this->articleModifyFormControlFactory->create($this->article);
	}
}

Toto řešení povoluje v Presenteru Article jen 3 akce, tedy přidání, editaci a zobrazení. (Pro jednoduchost berme, že zobrazení daného článku je přímo v šabloně presenteru a proto máme jen 1 komponentu a to pro formulář.)

V takto navrženém presenteru mluví akce samy za sebe, ale pro jistotu to ještě popíšu:

  1. Pokud uživatel přijde na akci přidat, tak pokud nemá práva editora a tedy nemůže přidat nový článek, tak jej zastaví už funkce ‚actionAdd‘. V případě, že má práva editora, tak se nastaví proměnná povolující vytvořit danou komponentu formuláře na true, což znamená, že podmínka projde v pořádku a tedy se komponenta v klidu vykreslí.
  2. Pokud uživatel přijde na akci editovat, tak presenter vytáhne daný článek z databáze a pokud článek neexistuje, nebo nemáme právo editovat tento konkrétní článek, tak nás samotná metoda ‚actionEdit‘ dále nepustí. Pokud ale článek existuje a právo máme, tak se opět nastaví proměnná povolující tvorbu komponenty a komponenta se vykreslí. (Komponenta je navržena tak, aby když jí jako parametr přijde null, jde o přidání, když ji přijde instance třídy, jde o editaci a o to, jestli může člověk tuto danou instanci editovat se už komponenta nestará (single responsibility. Komponenta se nemá starat o editaci a zároveň o práva.))
  3. Pokud uživatel přijde ovšem do akce detail, tak nás zase action metoda nepustí, pokud článek neexistuje, nebo nemáme právo jej vidět… Ovšem jelikož v akci detail se komponenta formuláře nepoužívá, tak logicky se nenastaví povolení komponentu vytvořit a tovární metoda pro tvorbu komponenty se vůbec nezavolá.
  4. Pokud uživatel nacházející se v akci přidání bude chtít odeslat formulář tak, aby se tvářil jako editace článku některého kolegy, ke kterému má i nemá přístup, tak mu toto aplikace nepovolí, protože komponenta, když je napsána tímto způsobem je spojena s danou akcí a to tak, že v akci add se nikdy nenaplní vnitřní proměnná ‚article‘ a tedy v rámci formuláře ať útočník dělá co chce se vždy bude jednat o přidání.
  5. Pokud uživatel nacházející se v akci editace bude chtít odeslat formulář tak, aby editoval jinou instanci (jiný článek), tak by musel pozměnit parametr ID v URL adrese, jenže tímto pozměněním jej nepustí již Action metoda, jelikož na editaci tohoto článku nemá oprávnění. Pokud by chtěl poslat request, který by vypadal jako přidání článku, tak je toto opět nemožné, jelikož v akci edit se vždy naplní Presenterovská vnitřní proměnná ‚article‘, takže se bude vždy jednat o editaci. Může takto editovat sice jiný článek, ke keterému má přístup na editaci, ale to ublíží jen sám sobě. Něco jako kdyby jste chtěli ukrást si samo sobě peníze ze svého druhého bankovního účtu…
  6. Pokud je v akci detail a pokusí se odeslat request, který odesílá formulář, tak se formulář bude tvářit, jako kdyby se snažil přidat článek. (Neplní se vnitřní proměnná article, takže si formulář myslí, že jde o přidání). Zde se stane to, že formulář se bude chovat jinak a tovární metoda ‚createComponentArticleModifyForm‘ se zavolá těsně po action metodě, která proběhne v pořádku (pokud je v detailu článku, který může vidět.) a ne až při vykreslování formuláře a to proto, že těsně po ‚action‘ metodách se volají signály (‚handle‘ metody), přičemž odeslání formuláře je taktéž signál a aby bylo možné formulář odeslat, je nutné jej nejdříve vytvořit… Jenže vzhledem k tomu, že v akci detail se nenastavuje proměnná ‚canCreateArticleModifyForm‘ na true, tak podmínka uvnitř továrničky na form selže a vyhodí error, který je vyhozen v signálu, tedy v čase, kdy se ještě nezačaly odesílat HTTP hlavičky na klienta a tedy je zde error správně, protože se neodeslaly ani hlavičky, ani se ještě nevytvořila komponenta formuláře, což šetří systémový čas a nemusí se nikde nic nijak odchytávat zevnitř komponent a komponenty se tím pádem stávají nezávislé na oprávněních.

V případech, kdy komponenta má v sobě komponentu se kterou na základě oprávnění můžeme pracovat je to také jednoduché. Například seznam mých článků, kde seznam je jedna komponenta a jednotlivý řádek/buňka je druhá komponenta. Zde chceme, abychom nemohli mazat články, které nejsou naše a tedy například abychom uvnitř Element komponenty mohli volat signál ‚handleDelete()‘ jen nad Elementy, které mazat můžeme.
Řešení je jednoduché.
Do první komponenty (Gridu chcete-li) si pošleme kolekci článků, které chceme zobrazit. Správnost této kolekce ověřuje již presenter, takže pro nevalidní data Grid komponentu vůbec nevytvoří… Tato Grid komponenta poté na validních datech vytvoří podkomponentu, nad kterou se zavolá daný signál. Pokud Grid komponenta ve své validní kolekci nemá element s daným ID (nemá danou podkomponentu, nad kterou chceme signál volat), nemůže tedy danou podkomponentu vytvořit a zobrazí se 404…

EDIT 1:
V případě, že by se v akci detail plnila vnitřní proměnná ‚article‘, tak se nic nemění. Formulář by si myslel sice, že jde o editaci, ale díky nenastavené proměnné ‚canCreate…‘ by se formulář vůbec nevytvořil, takže no stress.

EDIT 2:
A aby nevypadal presenter zaneřáděně a daly se použít komponenty ve více presenterech, tak je dobré cpát obslužné věci do trait. :)

Editoval Polki (8. 9. 2021 17:46)

Kamil Valenta
Člen | 757
+
0
-

Já si myslím, že rozkopírovávat to do action je rizikové (co když někdo u nějaké actiony zapomene) a špatně udržitelné. V checkRequirements() si pokryju všechny actiony z jednoho místa. Takže zapomenout na žádnou ani nemohu.

Polki
Člen | 553
+
-2
-

@KamilValenta

(co když někdo u nějaké actiony zapomene)

tak tímto můžeš argumentovat všude. Co když uživatel zapomene zaheslovat databázi? Co když zapomene vypnout debug mód? Co když zapomene nastavit práva v checkRequirements?

BTW. tohle řešení na to ovšem myslí a je blbuvzdorné…

Představ si pro tu komponentu traitu:

trait PresenterTrait
{
	#[Inject] public ControlFactory $articleModifyFormControlFactory;
	private ?Article $article = null; // Může být i ActiveRow
	private bool $canCreateArticleModifyForm = false;

	public function createComponentArticleModifyForm(): Control
	{
		if (!$this->canCreateArticleModifyForm) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		}
		return $this->articleModifyFormControlFactory->create($this->article);
	}
}

a Presenter:

class ArticlePresenter extends Presneter
{
    use PresenterTrait;

	#[Inject] public ArticleRepository $articleRepository;

	public function actionAdd(): void
	{
		if (!$this->user->isAllowed('article', 'add')) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		}
		$this->canCreateArticleModifyForm = true;
	}

	public function actionEdit(int $id): void
	{
		$article = $this->articleRepository->getById($id);
		if (!$article) {
			$this->error('Not found', IResponse::S404_NOT_FOUND);
		}
		if (!$this->user->isAllowed($article, 'edit')) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		}
		$this->canCreateArticleModifyForm = true;
	}

	public function actionDetail(int $id): void
	{
		$article = $this->articleRepository->getById($id);
		if (!$article) {
			$this->error('Not found', IResponse::S404_NOT_FOUND);
		}
		if (!$this->user->isAllowed($article, 'view')) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		}
		$this->template->article = $article;
	}
}
  1. ‚actionEdit‘ nezapomeneš napsat, protože vyžaduje parametr
  2. ‚actionDetail‘ nezapomeneš napsat, protože vyžaduje parametr
  3. ‚actionAdd‘ můžeš zapomenout napsat, ale to je v pořádku, protože v případě, že se na ni bude chtít KDOKOLIV dostat aniž by byly uvnitř ošetřená práva, bude mít zakázáno komponentu vytvořit a tedy vyskočí Error, který na to upozorní, takže si programátor v klidu vzpomene.

Výsledek: Jelikož jsou komponenty ve výchozím stavu zakázány a povolujeme je jen tam, kde je chceme pomocí action metod, nemůže nastat, že by někdo tam, kde chce komponentu použít zapomněl action metodu s právy napsat a zároveň když by opomněl, tak žádná bezpečnostní díra nehrozí. Nikdy.

Oproti tomu, jak by asi vypadalo checkRequirements?

class ArticlePresenter extends Presneter
{
	#[Inject] public ArticleRepository $articleRepository;

	public function checkRequirements($element): void
	{
        parent::checkRequirements($element);

		if ($this->isLinkCurrent('Article:add') && !$this->user->isAllowed('article', 'add')) {
			$this->error('Not allowed', IResponse::S403_FORBIDDEN);
		} elseif ($this->isLinkCurrent('Article:edit')) {
             $id = $this->getParameter('id');
             $article = $this->articleRepository->getById($id);
             if (!$article) {
			     $this->error('Not found', IResponse::S404_NOT_FOUND);
		     }
             if (!$this->user->isAllowed($article, 'edit') {
                 $this->error('Not allowed', IResponse::S403_FORBIDDEN);
             }
        } elseif ($this->isLinkCurrent('Article:detail')) {
             $id = $this->getParameter('id');
             $article = $this->articleRepository->getById($id);
             if (!$article) {
			     $this->error('Not found', IResponse::S404_NOT_FOUND);
		     }
             if (!$this->user->isAllowed($article, 'view') {
                 $this->error('Not allowed', IResponse::S403_FORBIDDEN);
             }
        }
        if ($this->isLinkCurrent('Nějak ověřím, jestli je odeslán signál od komponenty/odeslání formu????') &&
                  !$this->isLinkCurrent('Article:detail') &&
                  !$this->isLinkCurrent('Article:add')) {
             $this->error('Not allowed', IResponse::S403_FORBIDDEN);
        }
        if ...// další komponenty, které mohou být vykresleny za různých podmínek spolu i odděleně od formuláře a mají handly???
	}
}

Jinak si to moc nedokážu představit, jelikož každá akce ověřuje co uživatel může vůči jinému privilege a taky jedna to dělá prostě nad řetězcem jako resource, jedna nad entitou jako resource, takže se to horko těžko bude nějak sjednocovat a v takovém rozvětveném ifování se upřímně dá vyznat hůře, než v oddělených action metodách a tedy je to hůře udržitelné (co kdyby bylo akcí více? řekněme 40 a pak se nějaké privilege změní? Navíc ověřovat tady třeba to, jestli se náhodou nevolá signál komponenty, kterou volat nemám je řekněme strašně nepřehledné, složitě se přemýšlí co všechno a kde musím jak ověřit pro jaký případ a dá se do toho lehce zamotat a tedy něco opomenout. Jednoduše vyznat se ve stromě ifů je peklo, zatímco když je to v action metodách, tak hned člověk ví, kterou action metodu změnit, jelikož ví, pro co se změnilo privilege…). Proto se dnešní programy píší tak, aby v nich bylo co nejméně ifů a když, tak jen s jedním příkazem uvnitř o jednom zanoření. (Tím jsou myšleny takové ty ify, které ověří validitu vstupu a kdyžtak hned vyhodí výjimku, nebo ukončí vykonávání funkce.)

Řešení s checkRequirements se hodí do projektů, kde Presenter = Action. Tam je to super, protože je jeden jediný if a to ten, který by byl v action metodě je už v checkRequirements, která se volá o dost dříve, než action metoda.

PS:

V checkRequirements() si pokryju všechny actiony z jednoho místa. Takže zapomenout na žádnou ani nemohu.

z několika nezávislých zkušeností, kdy jsem seděl s nováčky a zkoušel je vím, že když se to nováček snažil dávat na jedno místo (do startupu, nebo do checkRequirements), tak hned, jak napsali kód, co byl v zadání, tak jsem jim řekl, aby připsali novou metodu, která něco dělá a má nějaká oprávnění.
Kromě 1 člověka VŠICHNI napsali action metodu, kde si předali parametry, napsali šablonu, zkusili refresh stránky, zjistili, že jsou na to vidět odkazy, tak odkazy skryly, ale upravit podle toho tu checkRequirements/startup už zapomněli, takže jsem jim na tu stránku přes odkaz normálně došel. V 99% se shodovaly i odpovědi, když jsem se zeptal, proč tedy neošetřili i přímý přístup přes URL. Odpovědi zněly nějak takto: „Já jsem to chtěl/a udělat nakonec a jelikož ověřování se děje ve startupu, tak jsem na to pak zapoměl/a, a myslel/a si, že už to tam je…“

Takže ze zkušenosti nováčkům dělá problém dopisovat nový kód do starých už hotových metod, které fungují. Naopak, když psali ověření do action metod, tak na to nezapomněli nikdy (opravdu ani jeden), protože věděli, že když už píšou action metodu pro předání parametrů, tak do ní rovnou rubnou i autorizaci/resourceGainCheck atp. a dávalo jim to i větší smysl, protože každá akce se opravdu starala jen sama o sebe a ne o ostatní…
Startup/checkRequirements tedy používáme jen pro ověření globálních pravidel, jako že musíš být admin na přístup do AdminModulu…

Aby sis nemyslel, že je to nějaký zeronumber, tak těch nováčků už jsem takto odzkoušel kolem 90.

Editoval Polki (8. 9. 2021 22:20)

Michal Kumžák
Člen | 106
+
+1
-

Osobně to řeším kombinací. Ve startup() provádím kontrolu zda je přihlášen a má právo na daný modul a pak v presenterech kontroluji zda má právo na danou akci. Další kontrolu provádím v případných akcích v komponentách, pokud akce vyžaduje jiné právo než co je v prezenteru.

Polki
Člen | 553
+
0
-

@KamilValenta Ale myslím si, že pořád jsou naše obě řešení ještě v normě.

Viděl jsem v jedné firmě borce Lukáše, který se tvářil, že zná vše, všechny a všude byl a razil názor že každý presenter má jen jednu akci a taky každý presenter musí být v separátním modulu a taky, že všechny callbacky formulářů má v jedné třídě FormService a jeho presenter vypadal nějak takto:

namespace App\Modules\UserModule\Presenters;

use ...;

final class SettingPresenter extends AbstractPresenter
{
	/** @var UserFormFactory @inject */
	public $userFormFactory;

	/** @var UserSettingFormFactory @inject */
	public $userSettingFormFactory;

	/** @var UserSettingFormService @inject */
	public $userSettingFormService;

	protected function createComponentUserForm(): Form
	{
		$form = $this->userFormFactory->create();
		$form->onAnchor[] = function (Form $form): void {
			$this->userFormFactory->setDefaultsByEntity($form, $this->user->identity);
		};

		return $form;
	}

	protected function createComponentUserSettingForm(): Form
	{
		$form = $this->userSettingFormFactory->create();
		$form->onAnchor[] = function (Form $form): void {
			$this->userSettingFormFactory->setDefaultsByEntity($form, $this->user->identity);
		};
		$form->onSuccess[] = function (Form $form, ArrayHash $values): void {
			$this->userSettingFormService->processForm($form, $values, $this->user->identity);
		};
		$form->onSuccess[] = function (Form $form, ArrayHash $values): void {
			$this->flashMessage('Nastavení změněno.', 'success');
			$this->redirect('this');
		};
		$form->onError[] = function (Form $form): void {
			foreach ($form->errors as $error) {
				$this->flashMessage($error, 'danger');
			}
		};

		return $form;
	}
}

Takže vlastně nastavoval callbacky, které se mají provést až v dané createComponent metodě, což jsem viděl bylo použito i v action metodách, takže se danému formuláři v action metodě nastavil success callback, což mělo za následek to, že když byl uživatel ve špatné akci, tak sice form odeslal, ale nic se nestalo, jelikož form neměl navěšenou onSuccess metodu… :D

EDIT 1:
userSettingFormService má v sobě jen 1 metodu a to processForm. No a pro každý existující formulář existuje taková třída zaregistrovaná jako service s 1 metodou jako callbackem formu :)

Editoval Polki (8. 9. 2021 22:42)

Polki
Člen | 553
+
0
-

@MichalKumžák Přesně tak.

Ve většině případů už uvnitř komponenty nemusím nic ověřovat, ale někdy to třeba je, když to chce uvnitř něco jiného jak píšeš.

Kamil Valenta
Člen | 757
+
0
-

Ano, v momentě, kdy si checkRequirements() napíšeš takto, tak tím skutečně můžeš demonstrovat jeho nevhodnost.
Pokud ho ale napíšeš nějak takto:

public function checkRequirements($element)
    {
        if (!$this->user->isLoggedIn()) {
            $this->getHttpResponse()->setCode(\Nette\Http\Response::S401_UNAUTHORIZED);
            $this->forward(':User:Auth:unauthorized', $this->storeRequest());
        }

        if (!$this->user->isAllowed($this->getReflection()->getName(), $this->formatActionMethod($this->action))) {
            $this->getHttpResponse()->setCode(\Nette\Http\Response::S403_FORBIDDEN);
            $this->forward(':User:Auth:forbidden');
        }

        parent::checkRequirements($element);
    }

tak tím můžeš demonstrovat jeho vhodnost. Je to součástí traity, která obdobným způsobem testuje i signály a rendery.
Takže skutečně nemusím checkRequirements() rozšiřovat, je obecný a pokrývá všechny actiony, handly a rendery.
Maximálně ho některé projekty ještě dědí a rozšiřují o getParameter() – to pro případ, že nechci rozslišovat jen mám / nemám přístup, ale i mám přístup na zázmam…

Kamil Valenta
Člen | 757
+
-2
-

@Polki moc nechápu, proč bych si měl checkRequirements() zanášet nějakým testováním, zda entita existuje a případně vykopávat 404. To má dělat router. A pokud mi to router pustil a neposlal to do 404, tak je URL legitimní. Myslím, že do sebe mícháš věci, které k sobě nepatří…

Polki
Člen | 553
+
0
-

Kamil Valenta napsal(a):

@Polki moc nechápu, proč bych si měl checkRequirements() zanášet nějakým testováním, zda entita existuje a případně vykopávat 404. To má dělat router. A pokud mi to router pustil a neposlal to do 404, tak je URL legitimní. Myslím, že do sebe mícháš věci, které k sobě nepatří…

To je diskutabilní.
Existují názory, kdy by router měl sahat do databáze pro entity a zjišťovat, jestli existují a na základě toho házet 404, nebo do Presenteru posílat přímo Entitu a odkaz generovat přímo z Entity.

Na druhou stranu existují názory, že router by se měl starat opravdu jen o routování a o nějakých entitách by neměl vůbec nic tušit, natož se na ně dotazovat do databáze. Validní URL podle tohoto je ta, která odpovídá požadovanému formátu i když záznam neexistuje a existenci záznamu zjistíme až tam, kde s ním chceme pracovat, tedy v presenteru.

Většina lidí preferuje 2 způsob a to tedy, že router se má starat jen o vzhled URL adres a netahat nic z DB a to i kvůli rychlosti. (například špatně navržený router může injectovat zbytečně mnoho services, může dělat zbytečné dotazy do DB a v neposlední řadě, pokud nemá uživatel právo manipulovat s nějakou resource, tak toto právo můžeme ověřit ještě před samotným dotazem do DB a ušetřit tak system time. Příklad je třeba že pokud jdeš na URL http://mujweb.cz/article/edit/7 a můžeš editovat články jen pokud jsi přihlášený, tak ověření přihlášení provádíš až v presenteru, přičemž v případě, že přihlášený nejsi úplně zbytečně v routě taháš článek s id 7, který poté předáváš do presenteru, jelikož presenter vyhodí error, not allowed…)
Pokud ale vezmeme v potaz i to, že máš použitý první způsob a děláš dotazy do DB rovnou v routeru, tak tvoje řešení je značně neúplné.

Vysvětlení:

if (!$this->user->isAllowed( // v pořádku.
        $this->getReflection()->getName(), // Tímto spojíš natvrdo název presenteru s názvem resource. To ale není pravda, jelikož na jedné stránce (v rámci 1 presenteru) můžeš mít ověření klidně vícero různých resources. Příkladem je třeba opět můj presenter s články. Pokud jsem v detailu článku, tak mám hned 2 resources. 1 je samotný článek a 2 jsou komentáře, kdy můžu třeba komentáře přidávat jen pokud jsem přihlášený/mám odběr/mám předplatné a stejné podmínky mohou být pro článek, ale každá může být jiná. Tímto řešením máš sice jednoduchý chceckRequirements, ale nechci si ani představovat, jak hrůzný a NEznovupoužitelný musí být ACL.
        $this->formatActionMethod($this->action), // Stejné jako předchozí řádek. Natvrdo spojuješ názvy akcí s privileges, což taky není optimální a navíc můžeš v rámci 1 akce chtít různě povolit/zakázat různé signály a touto konstrukcí se ptáš jen konkrétní presenter/akce. Zapomínáš ale, že hierarchie resource/privilege != presenter/action
    )) {
    $this->getHttpResponse()->setCode(\Nette\Http\Response::S403_FORBIDDEN);
    $this->forward(':User:Auth:forbidden');
}

BTW: handly nepokrývá. (nevím, co máš v metodě formatActionMethod, takže to nemůžu říct narovinu, ale ve výchozím stavu ne nepokrývá) a dělat si v ACL všechny možné kombinace preseter/action/signal které mohou nastat mi přijde jako systime sebevražda, ve které udělat chybu je snad ještě jednodužší, než v těch ifech, protože na nějakou kombinaci určitě někdy zapomeneš.

Editoval Polki (9. 9. 2021 1:09)

Kamil Valenta
Člen | 757
+
0
-

Já jsem vždycky opatrný při používání kvantifikace „většina lidí používá…“ – čím to máš podloženo?

K routerům: představ si dva moduly, oba mají routu ve tvaru: <domena>.<tld>/<slug>
Pokud první modul, resp. jeho router, nenajde mezi svými slugy ten požadovaný, pustí to dál, zkusí to druhý router, když ani ten, spadne to do 404.
Zjišťovat to až na úrovni presenteru je špatně, protože první router to vždycky matchne, pak v presenteru zjistí, že neměl a vykopne 404.

Samozřejmě že router nemusí sahat při každém requestu do db, může využívat cache. Argumentovat tím, že někdo router špatně napíše a pak to bude špatné je trochu mimo. Stejný argument můžeš přenést na cokoliv jiného. Když někdo napíše špatně actionu, tak to bude dobré?

Ad signály: psal jsem, že obdobným způsobem se testují signály a rendery. Je to jen fragment kódu.

Ano, checkRequirements() v mém případě spojuje názvy presenterů s resources. Protože mi to v 99 % případů stačí. A když nestačí, nic člověku nebrání zavolat si isAllowed() někde jine. Podívej se na svůj fragment kódu a uvidíš, že co já dělám automaticky, Ty děláš ručně. Ani ve Tvé ukázce odchylky nejsou…

Michal Kumžák
Člen | 106
+
+1
-

Kamil Valenta napsal(a):

Ano, v momentě, kdy si checkRequirements() napíšeš takto, tak tím skutečně můžeš demonstrovat jeho nevhodnost.
Pokud ho ale napíšeš nějak takto:

public function checkRequirements($element)
    {
        if (!$this->user->isLoggedIn()) {
            $this->getHttpResponse()->setCode(\Nette\Http\Response::S401_UNAUTHORIZED);
            $this->forward(':User:Auth:unauthorized', $this->storeRequest());
        }

        if (!$this->user->isAllowed($this->getReflection()->getName(), $this->formatActionMethod($this->action))) {
            $this->getHttpResponse()->setCode(\Nette\Http\Response::S403_FORBIDDEN);
            $this->forward(':User:Auth:forbidden');
        }

        parent::checkRequirements($element);
    }

tak tím můžeš demonstrovat jeho vhodnost. Je to součástí traity, která obdobným způsobem testuje i signály a rendery.
Takže skutečně nemusím checkRequirements() rozšiřovat, je obecný a pokrývá všechny actiony, handly a rendery.
Maximálně ho některé projekty ještě dědí a rozšiřují o getParameter() – to pro případ, že nechci rozslišovat jen mám / nemám přístup, ale i mám přístup na zázmam…

Tvoje řešení funguje jen do té doby, dokud nevyběhneš ze seznamu action. U mě je běžné, že například default, show, detail mají stejnou action read.

Kamil Valenta
Člen | 757
+
0
-

To je možné. Však já Ti nenutím kontrolovat oprávnění v checkRequirements(). Uvedl jsem to jako možnost.
S tím, že pokud člověk není autentikován, nebude se aplikace zdržovat s instancováním objektů ve startupu. To dosud nikdo nevyvrátil, ačkoliv někdo s tou odpovědí spokojený nebyl. Napsal jsem, že je to nejrychlejší cesta, kde uživatele ověřit. Nikdo zatím rychlejší neuvedl.
Neznamená to, že dál, později, hlouběji nemůžu ještě někde isAllowed() volat. To už jsem ale taky psal. Omlouvám se, ale už k tomu nemám víc co. Tazatel si jistě už vybral a svou cestu našel.

fikusir
Člen | 25
+
0
-

Jojo, už jsem si vybral :) Ověřování jsem prozatím přesunul do action a zároveň nechal v událostech komponenty. Asi existuje lepší řešení, ale v mých podmínkách si s tímto vystačím. Moc díky všem, diskuze je poučná

Polki
Člen | 553
+
0
-

@KamilValenta

Já jsem vždycky opatrný při používání kvantifikace „většina lidí používá…“ – čím to máš podloženo?

Oslovoval jsem firmy, které pracují s Nette a dělal analýzu jejich kódů. Mám za sebou přes 6000 webů a jen na 4 jsem viděl překlad slugů na entity přímo v routě, takže mám statistiky.

K routerům: představ si dva moduly, oba mají routu ve tvaru: <domena>.<tld>/<slug>

Tak tady záleží, jak kvalitně je appka navržená. opět, pokud se směřuje na různé presentery, tak máme i různé url adresy, takže tento problém nenastane. Viděl jsem ale už jednu aplikaci, která měla v action metodě 5 ifů a každý vykresloval jinou šablonu, protože byla url ve fotmátu jakém píšeš a slug byl někdy článek, jindy detail videa, jindy reklama z katalogu, ale v URL se posílalo jen UUID.
Ovšem tyto se řeší například tím, že se UUID v URL zašifruje pomocí klíče, který označuje daný presenter, nebo se UUID dá nějaký prefix označující presenter a tedy vůbec nemusíš nikam sahat, protože už samotná url v zašifrovaném UUID/id obsahuje informaci, o tom, do jakého presenteru směřuje.

Samozřejmě že router nemusí sahat při každém requestu do db, může využívat cache.

Problém je pořád ten samý. Musíš injectnout tunu services, vytvářet instance cachí, loadovat data z cache, ověřovat atp… A toto se dá velice snadno napsat špatně.

Argumentovat tím, že někdo router špatně napíše a pak to bude špatné je trochu mimo. Stejný argument můžeš přenést na cokoliv jiného. Když někdo napíše špatně actionu, tak to bude dobré?

Tady je to trošku jiné, než u mého stejného argumentu. Díky tomuto vrácení mi mého argumentu zpět se sice zvedají hodně šance, že projdeš Turingovým testem, ale reálně je tady ten argument nepoužitelný.
Pokud napíšu špatně router, tak jsem sice tupec úplně stejně, jako když napíšu špatně actiony, ale v případě špatně napsaného routeru tím zabiju celou aplikaci, zatímco špatně napsanou action metodou zabiju pouze tu konkrétní akci.

Ano, checkRequirements() v mém případě spojuje názvy presenterů s resources. Protože mi to v 99 % případů stačí.

To, že to stačí není dost dobrý argument pro to, to tak dělat.
Natvrdo to tím spojíš a jakákoliv změna se bude muset dělat na 2 místech místo na 1. Nehledě na to, jak píše Michal Kumžák ve spoustě akcí nezůstává u jednoho ověření. To, že v mém PŘÍKLADU to zrovna vyšlo podobně vůči akcím je čistě NÁHODA.
Pokud bych měl výpis všech článků a potom detail daného článku, tak je velmi pravděpodobné, že výpis i detail budou záviset na jedné privilege ‚view‘… Nebudu určitě pro to dělat vícero stejných privileges a čarovat s duplicitou kódu.
No a v neposlední řadě když přejdu z jenoho projektu na jiný, kde budu chtít použít ACL z druhého projektu, nebo budu chtít třeba použít presentery a ACL tam už bude, tak si nemyslím, že by bylo ok upravovat názvy resources a privileges podle aktuálního presenteru a zase to zamknout pospolu.

S tím, že pokud člověk není autentikován, nebude se aplikace zdržovat s instancováním objektů ve startupu.

Nemyslel jsi autorizován? Ať tak či tak, pořád platí, že obecné pravidla se tam hodí. Jako jestli je člověk přihlášen, nebo jestli má vůbec přístup k privilege, se kterou se v rámci daného presenteru pracuje. To je si myslím ok. Ale fixnout presenter na ACL si už myslím, že OK není.

Napsal jsem, že je to nejrychlejší cesta, kde uživatele ověřit. Nikdo zatím rychlejší neuvedl.

Hmm když tak máme rádi ten router… :D Myslím, že tam když to ověříš, jestli má člověk práva, tak nemusíš vůbec ani tvořit presenter a nemusíš toho člověka vůbec pustit dál a může 403 vykopnout rovnou router ne? Takže když jsou 2 moduly ve tvaru <domena>.<tld>/<slug>, přičemž do jednoho modulu uživatel právo přístupu má a do jiného ne, tak ten, kam právo přístupu nemá vyhodí rovnou 403 ne?
aneb:

Zjišťovat to až na úrovni presenteru je špatně, protože první router to vždycky matchne, pak v presenteru zjistí, že neměl a vykopne 404 403.

:D

Myslím, že pokud ti šlo jenom o vyvrácení toho, že je to nejrychlejší cesta, tak job done. Ne jak ale vidíš, tak ta nejrychlejší není vždy ta nejlepší.

Editoval Polki (9. 9. 2021 14:33)

Kamil Valenta
Člen | 757
+
+1
-

Ne, router nemá co autentikovat (a myslel jsem přesně to, co jsem napsal, tak já to většinou mám…)
Router má říct, jestli je URL legitimní, pokud ano, tak ji přeložit na presenter/action, pokud ne, nechat propadnout na 404.

O autentikaci a autorizaci se má starat ten, kdo odbavuje request, což je cotroller, resp. presenter.

Pokud to Ty vidíš jinak, tak to já respektuju a nemám potřebu Ti to vyvracet.

m.brecher
Generous Backer | 726
+
+1
-

Ahoj, tak zrovna já jako úplný nováček v Nette jsem se včera zamýšlel nad tím, že komponentou přes presenter by se dala obejít akce, která si sama oprávnění uživatele ověřuje v action<Action>.

Původně jsem veřejnou akci show a neveřejnou akci edit měl pohromadě v ArticlePresenter, oprávnění uživatele si hlídala neveřejná akce sama v action<Action> a já si myslel, jak to mám neprůstřelně zabezpečené.

Pak jsem tady na fóru četl, jak měl jeden kolega v aplikaci 9 let bezpečnostní díru protože popletl startup() a beforeRender() a jak se skutečně dalo do aplikace přes neautorizované založení nového administrátorského účtu dostat. Kolegové z fóra radili -dej to do startupu nebo do action<Action>

To nabouralo moje sebevědomí a uvědomil jsem si, že mě se může stát totéž, i když to nemám v beforeRender ale v actionEdit :).

Podobně jak píše @Polki ve své neprůstřelné analýze mne napadlo, že musím ohlídat slabé místo aplikace a to jsou komponenty a ověřit oprávnění rovnou v createComponent<Form>. Sice by to nějak fungovalo, ale nakonec jsem se rozhodl dodržet tyto zásady:

  • NIKDY v jednom presenteru nemíchat veřejné a neveřejné akce
  • VŽDY ověřovat oprávnění v metodě startup() presenteru

I když jak mě kolegové z opakovaně upozorňovali nikdy neříkej NIKDY, myslím, že jasná rigidní pravidla jsou pro nováčky úplně nejlepší. Pokročilejší uživatelé už vědí co a jak a můžou jít i komplikovanější cestou.

Ono je to pro nováčky ZÁLUDNÉ, na internetu, ve fóru a v dokumentaci se mnohokrát dočtou, že mají ověřovat ve startupu či action<Action> a když ověří oprávnění – tak v lepším případě v action<Action>, v horším případě v beforeRender, je to myslím dost častá chyba, když vizuálně nováček otestuje autorizaci tak se mu stránka nevykreslí a že se to dá přes signál obejít nenapadne skoro nikoho.

Ještě záludnější je, že i když má presenter jednu jedinou akci a oprávnění se ověřuje v action<Action>, tak se to dá obejít přes NEEXISTUJÍCÍ akci. Presenter VŮBEC NEOVĚŘUJE zda akce existuje, věří url a vykoná co je mu ZVENKU uloženo vykonat, obslouží signál, zavolá model, uloží/smaže data a pak se teprve pokusí vykreslit NEEXISTUJÍCÍ šablonu, protože akce neexistovala.

Komponenty jsou registrovány v CELÉM presenteru a nikoliv v konkrétní akci. Z tohoto konceptu vyplývá, že nejpřirozenějším řešením je ODDĚLIT veřejné a neveřejné akce do různých presenterů.

Takhle to může uspokojivě fungovat v nějakém jednoduchém CMS nebo eshopu, ale ve složitější aplikaci, kde je více uživatelů s různými složitými oprávněními už nelze takto oddělovat uživatele s různou úrovní oprávnění, protože to technicky není možné. Pak je řešením neprůstřelný Autorizátor, s jehož pomocí ověří Model všechny neveřejné operace s daty – oprávnění k tabulce, konkrétním sloupcům tabulky či konkrétním záznamům.

Nicméně v jednoduchých aplikacích, např. kde máme veřejný web a administraci je myslím postačující oddělit veřejné/neveřejné presentery, důkladně kontrolu oprávnění v startup metodách zkontrolovat a nemusíme se trápit s ověřováním oprávnění v Modelu.

Řešení jak navrhuje @Polki – nastavit povolení k vytvoření komponenty na false a v konkrétní metodě ho povolit je dokonalé a neprůstřelné. Systémovější ale je založit odděleně veřejné a neveřejné presentery – v souladu s tím, jak presenter SPOLÉHÁ na neprůstřelnou autorizaci ze strany vývojáře.

Díky @Polki za neprůstřelnou analýzu problematiky zranitelnosti komponent a vůbec zajímavý komplexní přístup k problematice. Než člověk napíše aplikaci v Nette pro ostrý provoz, měl by se hodně učit od zkušenějších kolegů :)

Polki
Člen | 553
+
0
-

@mbrecher
Nejvíc bezpečné je udělat si na každou jednu akci samostatný Presenter a tomu oprávnění řešit v startup, případně v checkRequirements, které se volá dřív, jak píše Kamil.

Pokud rozdělíš jen Presentery na veřejné a neveřejné, tak můžeš zase narazit.
Zkus si přepsat do startupu následující kód:

class ArticlePresenter extends Presenter
{
	#[Inject] public ArticleRepository $articleRepository;
	#[Inject] public ArticleFormFactory $articleFormFactory;
	private ?Article $article = null;

	protected function startup()
	{
		parent::startup(); // TODO: Change the autogenerated stub
		if (!$this->user->isAllowed('Article', 'view')) {
			$this->error('Bohužel nemáte oprávnění toto vidět', 401);
		}
	}

	public function actionAdd() {
		if (!$this->user->isAllowed('Article', 'add')) {
			$this->error('Bohužel nemáte oprávnění toto vidět', 401);
		}
	}

	public function actionEdit(int $id) {
		$this->article = $this->articleRepository->getById($id);
		if (!$this->article) {
			$this->error('Bohužel daný článek neexistuje', 404);
		}
		if (!$this->user->isAllowed($this->article, 'edit')) {
			$this->error('Bohužel nemáte oprávnění tento článek editovat', 401);
		}
	}

	public function createComponentArticleForm(): Form // jeden formulář jak pro editaci, tak pro přidání
	{
		return $this->articleFormFactory($this->article, function() {
			$this->flashMessage('Úspěšně odesláno', 'success');
			$this->redirect('this');
		});
	}
}

Tady to bude trošku složitější. Tento Presenter je neveřejný, jak píšeš, ale obsahuje 2 rozdílné akce, která má kadá úplně odlišná práva, jelikož řešíš, jestli článek, co edituješ náleží opravdu tobě.
Je to jako tady na fóru. Vytvářet odpovědi a editovat je můžeš jen když jsi přihlášený, tedy Presenter je neveřejný. Zároveň ale nesmíš být schopen editovat odpovědi jiných lidí. Kdyby jsi to mohl dělat, bylo by to blbé.

Řešení je:

  1. na každou akci jiný presenter (to ovšem bude presenterů jako blázen a vyznat se v tom není úplně ok máme to na projektu odzkoušené a mít v jedné složce 73 presenterů není úplně ok – a to nepočítám traity kvůli duplicitě kódu)
  2. narvat to do startupu všechno, ale to bude ifování jako pes, přičemž budeme zjišťovat, kdy kdo co může dělat, jelikož někdy článek je, jindy není, zjistit na základě jména akce jestli má být apod…
  3. mít prostě na jednu logickou část jeden presenter, tedy na všechnu práci s články bude presenter Article, který bude mít třeba 5 akcí, kde KAŽDOU tuto akci je třeba ověřit úplně stejně, jako v bodě 1, akorát nebude ověření rozkouskováno do 5 presenterů, ale bude v 1 a v tomto presenteru také dodělat ověření v KAŽDÉ createComponent metodě a v KAŽDÉ handle metodě s tím, že vykonávání VŠEHO vždy defaultně ZAKÁZAT a povolovat to jen tam, kde se to opravdu může použít.

Každopádně dobře jsi rozebral, jak to vidíš ty. Je to zase jiný pohled na věc, který vnese trošku světla do problematiky.

Editoval Polki (8. 10. 2021 1:00)

Polki
Člen | 553
+
0
-

@mbrecher
Ještě jsem zapomněl zmínit, že máš pravdu a když má presenter jen 1 akci, tak toto:

<?php

declare(strict_types=1);

namespace App\Presenters;

use Nette\Application\UI\Presenter;

final class HomepagePresenter extends Presenter
{

	public function actionDefault(): void
	{
		if ($this->user->isLoggedIn()) {
			$this->error();
		}
	}

	public function handleSignal(): void
	{
		bdump('ahoj');
	}

}

Je nedostatečné, protože teoreticky se to dá obejít přes neexistující akci, například když do URL zadám /homepage/neexistujici-akce?do=signal
tak Nette správně vyhodnotí, že Page not found. Missing template 'D:\wamp64\www\testNette31\app\Presenters\templates\Homepage\neexistujiciAkce.latte'., ale handle metoda se provede a řetězec ahoj se dumpne.

Řešení problému s neexistujícími akcemi je to, že z routeru odebereš obecnou routu a necháš routing routovat vždy jen ty routy, které existují. Takže v tomto případě když bude router vypadat třeba takto:

final class RouterFactory
{
	use Nette\StaticClass;

	public static function createRouter(Nette\Security\User $user): RouteList
	{
		$router = new RouteList;
		$router->addRoute('homepage', 'Homepage:default');
		//$router->addRoute('<presenter>/<action>[/<id>]', 'Homepage:default'); // odebrána obecná routa
		return $router;
	}
}

tak se nikdo přes neexistující akci k signálu nedostane, jelikož ten dotaz na tu URL vyhodí No route for HTTP request.
Otázka potom je, co si chce člověk vybrat.

  1. univerzální routu, 1 presenter = 1 akce, ověřování ve startup metodě
  2. konkrétní routy, 1 presenter = 1 akce, ověřování v action metodě, kde už máme přístupné parametry z URL
  3. dělat to nějakým jiným způsobem z těch, které jsou definované v minulém komentáři.
Marek Znojil
Člen | 76
+
0
-

Polki napsal(a):

@mbrecher
Ještě jsem zapomněl zmínit, že máš pravdu a když má presenter jen 1 akci, tak toto:

<?php

declare(strict_types=1);

namespace App\Presenters;

use Nette\Application\UI\Presenter;

final class HomepagePresenter extends Presenter
{

	public function actionDefault(): void
	{
		if ($this->user->isLoggedIn()) {
			$this->error();
		}
	}

	public function handleSignal(): void
	{
		bdump('ahoj');
	}

}

Je nedostatečné, protože teoreticky se to dá obejít přes neexistující akci, například když do URL zadám /homepage/neexistujici-akce?do=signal
tak Nette správně vyhodnotí, že Page not found. Missing template 'D:\wamp64\www\testNette31\app\Presenters\templates\Homepage\neexistujiciAkce.latte'., ale handle metoda se provede a řetězec ahoj se dumpne.

Řešení problému s neexistujícími akcemi je to, že z routeru odebereš obecnou routu a necháš routing routovat vždy jen ty routy, které existují. Takže v tomto případě když bude router vypadat třeba takto:

final class RouterFactory
{
	use Nette\StaticClass;

	public static function createRouter(Nette\Security\User $user): RouteList
	{
		$router = new RouteList;
		$router->addRoute('homepage', 'Homepage:default');
		//$router->addRoute('<presenter>/<action>[/<id>]', 'Homepage:default'); // odebrána obecná routa
		return $router;
	}
}

tak se nikdo přes neexistující akci k signálu nedostane, jelikož ten dotaz na tu URL vyhodí No route for HTTP request.
Otázka potom je, co si chce člověk vybrat.

  1. univerzální routu, 1 presenter = 1 akce, ověřování ve startup metodě
  2. konkrétní routy, 1 presenter = 1 akce, ověřování v action metodě, kde už máme přístupné parametry z URL
  3. dělat to nějakým jiným způsobem z těch, které jsou definované v minulém komentáři.

Prezentery s jednou akcí můžeš řešit i tak, že upravíš metody ve výchozím prezenteru či traite tomu určené, které vrací aktuální název akce.

Počítáno s tím, že v routě máš výchozí akci default.

https://github.com/…resenter.php#L494

    public static function formatActionMethod(string $action): string{
      if($action !== parent::DEFAULT_ACTION){
          throw new Nette\Application\BadRequestException();
      }

        return 'action';
    }

https://github.com/…resenter.php#L503

    public static function formatRenderMethod(string $view): string{
        return 'render';
    }

Výhoda? Nemusíš používat názvy metod actionDefault ani renderDefault, ale pouze action a render.

Editoval Marek Znojil (8. 10. 2021 9:23)

Polki
Člen | 553
+
+1
-

@MarekZnojil
U toho, když ma 1 presenter 1 akci to samozřejmě znemožní zkusit použít jiné akce, než je default, takže to taky zabrání spuštění signálu z jiné akce.

Ovšem najdou se i lidé, kteří řeknou, ze když je to v tomto případě jednoznačné, že existuje jen akce default, tak by se o to měl starat router, takže buď odebrat obecnou routu, nebo ji tam hodit filtr na akci, který pusti pouze default akci, takže se vubec nebude případně tvořit presenter.
Nicméně přejmenování metod na čistě action a render mi přijde stylové.

Marek Znojil
Člen | 76
+
+1
-

Polki napsal(a):

@MarekZnojil
U toho, když ma 1 presenter 1 akci to samozřejmě znemožní zkusit použít jiné akce, než je default, takže to taky zabrání spuštění signálu z jiné akce.

Ovšem najdou se i lidé, kteří řeknou, ze když je to v tomto případě jednoznačné, že existuje jen akce default, tak by se o to měl starat router, takže buď odebrat obecnou routu, nebo ji tam hodit filtr na akci, který pusti pouze default akci, takže se vubec nebude případně tvořit presenter.
Nicméně přejmenování metod na čistě action a render mi přijde stylové.

Máš pravdu a souhlasím s tebou. Pouze jsem chtěl poukázat i na tuto možnost, jelikož tímto vyřešíš oba případy. Omezíš akce prezenteru pouze na jednu a zkrátíš název metod.

Správnější (?) přístup z logiky věci řešit jaká akce má být zobrazena je v routě. Řešení na které jsem poukázal je spíše pohodlné, protože budeš mít jednu traitu (nebo BasePresenter), kterou si použiješ dle libosti a nemuší řešit individuální routy pro každý prezenter zvlášť u kterého chceš, aby byl ‚single action‘.

Polki
Člen | 553
+
+1
-

Tak routu můžeš mít pro všechny Presentery jednu a rozhodovat překladovým slovníkem, co se použije.
Tedy nadefinuješ něco jako:

final class RouterFactory
{
	use Nette\StaticClass;

	public static function createRouter(Nette\Security\User $user): RouteList
	{
		$router = new RouteList;
		$router->addRoute('<presenter>[/<id>]', [
			'presenter' => [
				Route::VALUE => 'Homepage',
				Route::FILTER_TABLE => [
					'pridat-clanek' => 'ArticleAdd',
					'editace-clanku' => 'ArticleEdit',
					'detail-clanku' => 'ArticleDetail',
					// Výčet všech dalších presenterů, které existují
				],
				Route::FILTER_STRICT => true,
			],
			'action' => 'default',
		]);
		return $router;
	}
}

Což je pak úplně jedno jaká je URL, protože akce se vždy přeloží do default. Na základě url se mění jen použitý presenter.
Snad jen pro různé očekávané parametry v URL adrese bych dělal různé routy. Tedy presentery, které ID nepotřebují by měli svou routu a ty, které jej potřebují také svou, tedy něco jako:

$router->addRoute('<presenter>/<id>', [
	'presenter' => [
		Route::VALUE => 'Homepage',
		Route::FILTER_TABLE => [
			'editace-clanku' => 'ArticleEdit',
			'detail-clanku' => 'ArticleDetail',
			// Výčet všech dalších presenterů, které existují a berou ID parametr
		],
		Route::FILTER_STRICT => true,
	],
	'action' => 'default',
]);
$router->addRoute('<presenter>', [
	'presenter' => [
		Route::VALUE => 'Homepage',
		Route::FILTER_TABLE => [
			'pridat-clanek' => 'ArticleAdd',
			// Výčet všech dalších presenterů, které existují a neberou id parametr
		],
		Route::FILTER_STRICT => true,
	],
	'action' => 'default',
]);

Jde o to, aby nebyla validní třeba URL typu /pridat-clanek/15, která nedává smysl.
Řešení, které jsi nastínil ty se mi tedy líbí, ale spíše pro to, že jelikož je akce jen 1, tak odpadne psaní default názvů. :)

Editoval Polki (8. 10. 2021 12:07)