Best practices: ACL, Autentizaci, autorizaci…

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

Nemáte někdo komplexní příklad (best practices) pro ACL, Autentizaci, autorizaci a řízení kontroly přístupu do jednotlivých Preseneteru atd.

Nějaké fajn střípky a náznaky tady už jsou (Jak resite kontrolu opravneni), ale nic komplexnějšího. Zkoušel jsem se tím dneska prokopat podle návodu, ale asi mi prostě stále něco uniká :-(

Akrobata jsem si prošel. Nezdá se mi moc DRY kontrolovat v každém Presenteru/Startup oprávnění přístupu.

Takže myslím, že by to mohlo fungovat nějak takhle:

  • V tabulce Users přidám atribut role – (tam bude např. quest, admin atd.)
  • Models/Users funkce authenticate na konci navíc vrátí return new Identity($row->email, $row->role, $row);
<?php
// Models/Users.php
class Users extends Object implements IAuthenticator
{
	public function authenticate(array $credentials)
	{
		// input
		$username = strtolower($credentials[self::USERNAME]);
		$password = strtolower($credentials[self::PASSWORD]);

		// search
		if (StrValidate::email($username)) {
			// autenticate by e-mail
			$row = dibi::select('*')->from('user')->where('email=%s', $username)->fetch();
		} else {
			// autenticate by username
			$row = dibi::select('*')->from('user')->where('username=%s', $username)->fetch();
		}

		// user validate
		if (!$row) {
			throw new AuthenticationException("Uživatel '$username' nenalezen.", self::IDENTITY_NOT_FOUND);
		}

		// password validate
		if ($row->password !== md5($credentials[self::PASSWORD])) {
			throw new AuthenticationException("Neplatné heslo.", self::INVALID_CREDENTIAL);
		}
		// unset password
		unset($row->password);

		// return identity
		echo $row->role;
		return new Identity($row->email, $row->role, $row);
	}

}
  • V bootstrap.php bude potřeba konstruovat ACL a patřičně jej nastavit.
// cast bootstrap.php

$acl  = Environment::getService('Nette\Security\IAuthorizator');
$acl->addRole('admin');
$acl->addRole('quest'); // quest nemůže vše

$acl->addResource('dashboard'); // presenter
$acl->addResource('config'); // presenter

$acl->allow('admin'); // admin muze vse
$acl->allow('quest', array('dashboard')); // quest muze jen vse na dashboard

//echo $acl->isAllowed('admin', 'config') ? "allowed" : "denied";
  • BasePresenter by asi měl kontrolovat přístupy/oprávnění (asi ve funkcích beforeRender nebo startup)

V config.ini mám toto

service.Nette-Security-IAuthenticator = Users
service.Nette-Security-IAuthorizator  = Permission

První řádek chápu. Ten druhů dělá přesně co? Kde bude hledat třídu Permission?

Rád bych to všechno napsal dobře :-) a podělil se s ostatními.

Honza Marek
Člen | 1664
+
0
-
service.Nette-Security-IAuthorizator = Permission

nastaví službu Nette\Security\IAuthorizator tak, aby ji obstarávala třída Permission. Hledá ji autoloader.

Místo nastavování Permission v bootstrapu bych vytvořil poděděnou třídu k Permission, třeba MyPermission a ty pravidla nastavil v konstruktoru. Pak stačí v configu nastavit

service.Nette-Security-IAuthorizator = MyPermission

a nezasírá se bootstrap. Přijde mi to tak lepší :-)

zakjan
Člen | 9
+
0
-

Jedna poznámka – quest nebo guest? ;)

Roman Ožana
Člen | 52
+
0
-

Myslím že guest i když to je Quest :)

Roman Ožana
Člen | 52
+
0
-

To: Honza M.

  • a nezasírá se bootstrap. Přijde mi to tak lepší :-) – souhlasím a díky za tip

To: Jan Tvrdík

  • procházel jsem, ale chtěl bych to nastavovat natvrdo (alespoň zatím)
Roman Ožana
Člen | 52
+
0
-

Takže slíbené (moje) best practices:

První věc ACL – pro správu oprávnění a rolí

<?php
class ACL extends Permission
{
	function  __construct()  {
		$this->addRoles(); // add roles
		$this->addResources(); // add all resources

		$this->allow('admin'); // admin muze vse
		$this->allow('guest', array('Dashboard')); // quest muze jen vse na dashboard
	}

	/**
	 * Add all Resources
	 */
	function addResources() {
		$this->addResource('Dashboard'); // takhle mám pojmenované presentery
		$this->addResource('Config'); // ConfigPresenter.php
		$this->addResource('Client');
	}

	/**
	 * Add all roles
	 */
	function addRoles()
	{
		$this->addRole('guest'); // moje role
		$this->addRole('admin');
	}
}

?>

ACL.php – umístil jsme do složky presenters (možná by se mu dalo najít i lepší místo)
a v config.ini jsem přidal /service.Nette-Security-IAuthorizator = ACL/

Druhá věc boj s uživatelem:

CREATE TABLE  `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `email` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  `password` varchar(45) COLLATE utf8_czech_ci NOT NULL,
  `username` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  `real_name` varchar(128) COLLATE utf8_czech_ci NOT NULL,
  `role` varchar(128) COLLATE utf8_czech_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

to máme tabulku users a model

<?php
class Users extends Object implements IAuthenticator
{
	public function authenticate(array $credentials)
	{
		// input
		$username = strtolower($credentials[self::USERNAME]);
		$password = strtolower($credentials[self::PASSWORD]);

		// search
		if (StrValidate::email($username)) {
			// autenticate by e-mail
			$row = dibi::select('*')->from('user')->where('email=%s', $username)->fetch();
		} else {
			// autenticate by username
			$row = dibi::select('*')->from('user')->where('username=%s', $username)->fetch();
		}

		// user validate
		if (!$row) {
			throw new AuthenticationException("Uživatel '$username' nenalezen.", self::IDENTITY_NOT_FOUND);
		}

		// password validate
		if ($row->password !== md5($credentials[self::PASSWORD])) {
			throw new AuthenticationException("Neplatné heslo.", self::INVALID_CREDENTIAL);
		}
		// unset password
		unset($row->password);

		// tady nacitam taky roli
		return new Identity($row->email, $row->role, $row);

	}

}
?>

Presenter, který se stará o přihlášení

<?php
class AuthPresenter extends Presenter
{
	/** @persistent */
	public $backlink = '';


	public function actionLogin($backlink)
	{
		$form = new AppForm($this, 'form');
		$form->addText('username', 'Uživatelské jméno:')
			->addRule(Form::FILLED, 'Zadejte uživatelské jméno, nebo e-mail.');

		$form->addPassword('password', 'Heslo:')
			->addRule(Form::FILLED, 'Please provide a password.');

		$form->addSubmit('login', 'Přihlásit');
		$form->onSubmit[] = array($this, 'loginFormSubmitted');

		$form->addProtection('Prosím přihlašte se znovu.');

		$this->template->form = $form;
		$this->template->title = "Přihlásit se";
	}

	public function renderLogout()
	{
		Environment::getUser()->signOut();
		$this->flashMessage('Byl jste úspěšně odhlášen.');
		$this->redirect('Auth:login');
	}

	public function loginFormSubmitted($form)
	{
		try {
			$user = Environment::getUser();
			$user->authenticate($form['username']->getValue(), $form['password']->getValue());
			$this->getApplication()->restoreRequest($this->backlink);
			$this->redirect('Dashboard:');

		} catch (AuthenticationException $e) {
			$form->addError($e->getMessage());
		}
	}
}
?>

jo a v config.ini řádek /service.Nette-Security-IAuthenticator = Users/

Nakonec jsem do BasePresenter (po kterém dědí všechny ostatní presentery, kromě AuthPresenter)
tohle:

<?php
abstract class BasePresenter extends Presenter
{


	protected function startup()
	{
                // tady kontroluju jestli mam opravneni zobrazit
		if (!Environment::getUser()->isAllowed($this->name, $this->view))
		{
			//throw new InvalidStateException();
			$this->redirect('Auth:logout'); // fuj přihlaš se
		} else {
			echo 'ANO OK mas na to pravo';
		}
	}
}
?>

Editoval Roman Ožana (3. 5. 2009 21:16)

Jerry123456789
Člen | 37
+
0
-

Pěkný Best Practices, jen jedna drobnost: nikde nemáš storeRequest a v logoutu (souvisí to s tim storem) do actionLogin nepředáváš $key ze storu.
//edit: a ještě mi nepřijde dobré upozorňovat uživatele, který se nikdy nepřihlásil (ani nezaregistroval) „Byl jste úspěšně odhlášen“

Editoval Jerry123456789 (25. 5. 2009 16:58)

wdolek
Člen | 331
+
0
-

jak je to s „guest“ uctem? tj aby kazdy kdo jen vleze na stranku mel guesti prava? – resit nekde v aplikaci jestli je user NULL nebo ma prava mi prijde „komplikovane“ (proc to nemit v jednom)… ?

Ondřej Mirtes
Člen | 1536
+
0
-

Stačí volat Environment::getUser()->isAllowed($resource, $privilege) a je to pořešené podle jeho role…

Lábus
Bronze Partner | 19
+
0
-

v příkladu dáváte ověření identity do funkce startup() v BasePresenteru… jak resite situaci, kdy potrebujete pouzit startup() (napriklad pro nacteni nejake konfigurace) v jinem presentu, ktery od BasePresenteru dedi?

jasir
Člen | 746
+
0
-

Lábus napsal(a):

v příkladu dáváte ověření identity do funkce startup() v BasePresenteru… jak resite situaci, kdy potrebujete pouzit startup() (napriklad pro nacteni nejake konfigurace) v jinem presentu, ktery od BasePresenteru dedi?

<?php
class MyPresenter extends BasePresenter {
  public function startup() {
     parent::startup();
     ...tvůj kód...
  }
}
?>

:-)

Ondřej Brejla
Člen | 746
+
0
-

To jsou základy dědičnosti, zkuste si pročíst některý ze článků zabývající se objekty v PHP. Ujasníte si určitě spoustu věcí. Z hlavy mě nenapadá žádný určitý, ale po troše googlení se jistě něco najde.

kravčo
Člen | 721
+
0
-

Práve na tento problém som prednedávnom narazil aj ja. Píšem „problém“, pretože keď niekde jedno volanie parent::startup() zabudnem, mám problém a s trochou šťastia i slušnú bezpečnostnú dieru…

Jedným z riešení je deklarovať metódu final startup() na mieste, kde sa kontrolujú práva, problém je, že ďalej ju už nemôžem rozširovať, čo môže byť niekedy obmedzujúce.

Vyriešiť sa to dá napríklad trojicou metód:

final protected function startup()
{
    $this->verify();
    $this->initialize();
}

final protected function verify()
{
    // ...
}

protected function initialize()
{
}

Toto mi ale príde vcelku komplikované a nepohodlné, pričom nemôžem využívať zažitú metódu startup() a namiesto nej mám metódu initialize()

To, čo mi ešte napadlo je rozšíriť životný cyklus prezenteru o metódu určenú na overovanie práv, ktorú by bolo možné na vhodnom mieste deklarovať final a tým znemožniť neúmyselné obídenie tejto kontroly.

public function run()
{
    ...
    $this->verify();
    $this->startup();
    ...
}

Čo si o tom myslíte?

Ondřej Brejla
Člen | 746
+
0
-

Já myslím, že zařazení metody do životního cyklu pro kontolu práv je dobrý nápad…ale nemyslím si, že je nutnost tuto metodu deklarovat jako final, jasně, je to lepší…„nevědomky si jí nepřekryju“, ale není to nutnost…rozhodně bych její finalitu nevynucoval…„co kdyby náhodou někde…“;-) Navíc pokud se dobře zvolí název, tak je minimální šance, že jí překryju neúmyslně. Ale pro zavedení rozhodně jsem. Já startup() používám k inicializaci modelů a pokaždé si vzpomenout, že musím volat parent…nic moc. Takže metodu pro práva určitě ano.

kravčo
Člen | 721
+
0
-

Warden napsal(a):

…ale nemyslím si, že je nutnost tuto metodu deklarovat jako final, jasně, je to lepší…„nevědomky si jí nepřekryju“, ale není to nutnost…rozhodně bych její finalitu nevynucoval…„co kdyby náhodou někde…“;-)

Framework samozrejme finalitu metódy vynucovať nemôže – v takom prípade by nebola veľmi použiteľná… Príkladom som sa snažil ukázať, že finálna by mala byť až vlastná implementácia a s predpokladom takejto metódy v životnom cykle to bude podstatne jednoduchšie.

class BasePresenter extends Presenter
{
     final protected function verify()
     {
         // verification
     }

     protected function startup()
     {
         // initialization
     }
}
Jod
Člen | 701
+
0
-

Trošku zbytočné

PetrP
Člen | 587
+
0
-

Myslím že jen malá část lidí to ocení, zvláště kdyby to bylo svázané jen na oveření práva. Napadá mě ale přidat před startup událost onStartup stejně jako to je u shutdown:

// Presenter::run();

$this->onStartup($this);
$this->startup();
...
$this->onShutdown($this, $e);
$this->shutdown($e);

Byla by už pak na tobě co si budeš volat jestli inicialize, verify, whatever

Ondřej Brejla
Člen | 746
+
0
-

kravco napsal(a):

Jj já vim jak si to myslel, jen sem nechtěl, aby se ona finalita této metody nedostala do jakých si Best practices (viz. nadpis vlákna) a stalo se z toho nějaké dogma. Jinak s tebou souhlasím.

K tomu jestli zbytečné nebo ne…teď tu máme jiné opravdu zbytečné metody prepare atd…ty se nevyužívají, tohle by se imho využívalo, jakmile by si na to lidé zvykli ;-)

romansklenar
Člen | 655
+
0
-

Souhlasím s Petrem, událost onStartup je vhodnější než zesložiťovat životní cyklus presenteru. Chtělo by to ale, aby byla přímo ve frameworku.

EDIT: akorát mě nenapadá, kde tu událost nadefinovat… Hned poté, co je aplikaci presenter známý tak ho spouští.

PetrP
Člen | 587
+
0
-

romansklenar napsal(a):

Souhlasím s Petrem, událost onStartup je vhodnější než zesložiťovat životní cyklus presenteru. Chtělo by to ale, aby byla přímo ve frameworku.

EDIT: akorát mě nenapadá, kde tu událost nadefinovat… Hned poté, co je aplikaci presenter známý tak ho spouští.

asi jedině v konstructoru (to není nejlepší), nebo takto:

class BasePresenter extends Presenter
{
	public $onStartup = arary(array(__CLASS__,'initialize'));
	static public function initialize($_this)
	{
		$_this->doSomething();
	}
}

To má zase nevýhodu že můze přistupovat jen k public věcem presenteru.

David Grudl
Nette Core | 8082
+
0
-

A co zajistit/vynutit, že bylo voláno parent::startup()? Na to by se nějaký obecný mechanismus udělat dal.

PetrP
Člen | 587
+
0
-

Jako něco takového?

//Presenter
public function startup()
{
	$this->bylVolanParentStartup = true;
}
//Presenter::run()
$this->startup();
if ($this->bylVolanParentStartup !== true)
	throw new WtfException('Co děláš déžo, nezavolal si `parent::startup()`, to se mi ale vůbec nepáčí!!!!');
David Grudl
Nette Core | 8082
+
0
-

Hele mě se to líbí ;)

Mám začít vyhazovat warning, když se nezavolá parent::startup()? Co myslíte? Jsem pro!

Jod
Člen | 701
+
0
-

To by asi viac vecí ulahčilo než zťažilo.

washo
Člen | 88
+
0
-

Jeste k Best practice…

proc je v tride Users v methode authenticate

$password = strtolower($credentials[self::PASSWORD]);

?

lactarius
Člen | 47
+
0
-

Protože třída Users implementuje určitý rozhraní a ty musíš přizpůsobit formát.

Já jsem narazil na daleko horší problém. Nefunguje mi odhlašování. Kdyby jenom to – nefunguje mi odhlašování pouze u některých uživatelů.

Metodu Logout mám v AuthPresenteru (zkoušel jsem ji přemístit do BasePresenteru – marně)

<?php
  public function actionLogout()
	{
		Environment::getUser()->signOut();
		$this->getApplication()->restoreRequest($this->backlink);
	}
?>

Napadlo mě, jestli to nemůže být prostředím (teď přijde něco, co vůbec nepatří do tohoto threadu, tak mi moc nenadávejte – myslím si totiž, že vůbec nejsem sám, kdo na podobnou věc v poslední době narazil) – před pár dny mě napadlo, že je třeba provést upgrade – Apache, PHP, MySQL. To jsem ještě netušil, že poslední verze MySQL, kterou se mi podaří nainstalovat, je 5.0.65. Pak jsem brouzdáním po světě zjistil, že se jedná o mezinárodní záležitost – jeden hinduista z Bombaje dokonce všem radil, ať si nainstalují verzi 4.1, že to na doma stačí. Nicméně – nakonec jsem byl nucen (i když velmi nerad) sáhnout po nějaké triádě – WampServer5 se to jmenuje. Od té doby se to chová divně. Kdybyste někdo měl nápad…

Ale to jsem fakt odbočil – zpátky k velmi pěknému Best Practice – mělo by jich tu být víc.

Co se týče isAllowed, řeším ve Startupu v BasePresenter:

<?php
	protected function startup()
	{
		$name = $this->name;
		$view = $this->view;

		//If this resource isn't allowed => get out
		$user = Environment::getUser();
		if(!$user->isAllowed($name, $view)) {
			$this->getApplication()->restoreRequest($this->backlink);
		}
		//View username | login form
		if($user->isAuthenticated()) {
			$this->template->userlogged = $user->getIdentity()->getName();
		} else {
			$this->template->loginform = $this->getComponent('loginForm');
		}

		parent::startup();
	}
?>

Jednoduchým způsobem – uživatele to při nedostatku práv neodhodí někam na přihlášení – přihlašovací formulář je fixně v pravém sloupci layoutu. Uživatel prostě a jednoduše zůstane kde je. Daleko lepší by ovšem bylo, kdyby se odkazy mohly dynamicky přizpůsobit tomu, kdo je přihlášen.
Tj. bude-li přihlášen guest, nezobrazí se mu vedle položky z databáze ikona z odkazem pro úpravu. Kdyby se naopak přihlásil admin, zobrazila by se jak ikona pro úpravu, tak pro výmaz.
Určitě už jste to někdo řešil – já samozřejmě taky, ale to ještě nebylo v Nette.

Milhauz
Člen | 26
+
0
-

Dynamiké zobrazení odkazů je obdobné. Řešim to tak, že do template uložim přihlášeného uživatele.

<?php
$this->template->loggedUser = $user;
?>

A potom v template u daného odkazu přidám podmínku

<?php
{if $loggedUser->isAllowed('name','view')}
  <a href=...
{/if}
?>
lactarius
Člen | 47
+
0
-

Jé – to je pěkný – přesně tohle jsem měl namysli. Takže v šabloně. Akorát – není to trochu proti filozofii „V šablonách se neprogramuje“ ?

Ondřej Mirtes
Člen | 1536
+
0
-

lactarius napsal(a):

Jé – to je pěkný – přesně tohle jsem měl namysli. Takže v šabloně. Akorát – není to trochu proti filozofii „V šablonách se neprogramuje“ ?

Pokud chceš mít „čistší“ šablony, můžeš si v Presenteru dát do šablony přímo proměnnou, která ti bude indikovat povolení/zákaz:

$this->template->isAllowed = $user->isAllowed('name', 'view');

A v šablonách se ptát:

{if $isAllowed}
protected HTML code
{/if}
Klokan
Člen | 47
+
0
-

Na školení se řešilo, že nejlepší je mít uvnitř presenteru funkci, která pak tento problém řeší komplexně na jednom místě. Když se potom změní způsob ověřování, udělá se to na jednom místě a je to všude ošetřené. Tj. presenter

<?php
  public function isAllowed($name,$view) {
    $this->user->isAllowed($name, $view)
  }

  public function renderDefault() {
    if($this->isAllowed(.....)
  }
?>

a v šabloně

<?php
  {if($presenter->isAllowed(....)}
   ... neco povol .....
  {/if}
?>
Oggy
Člen | 306
+
0
-

Jak řešíte/ byste řešili tyto práva:

Dejme tomu, že máme nějakou kategorii ..
Máme nějaké role.. admin, editor, guest
Každý má nějaké práva..
a pokud bychom chtěli nastavit, že editor má právo ještě omezené třeba jen na nějakou kategorii..

takže

admin – všechny operace se všemi kategoriiemi
editor – všechny operace s tabulkami týkající se nějaké kategorie
další editor – všechny operace s tabulkami týkající se nějakých kategorií

jednoduše. ty práva, která jsou v dokumentaci popsána rozšířit (nebo spíše zúžit) o to omezení záznamy v db týkající se jen některých kategorií..

snad je to srozumitelné:-)

hrach
Člen | 1834
+
0
-

ten link by to chtělo dát do dokumentace. před týdnem mi taky trvalo, než sem to našel…

couda
Člen | 9
+
0
-

zdravím,

tak to tady tak pročítám, hledám a stále mi to nefunguje. Udělal jsem vše podle best practices na 1. straně, ale jakmile kdekoliv uvedu $user->isAllowed('neco','neco') tak mi nette vyhodí chybu „Cannot redeclare class BasePresenter“. Asi bude chyba, že se mi někde 2× dotazuje loader.php nebo něco podobného, ale nemůžu přijít na to kde.

nevíte co s tím?

Roman Ožana
Člen | 52
+
0
-

Problém bude v abstract class BasePresenter extends Presenter

Zřejmě již takovou Class ve své aplikaci máte

SquirrelCZE
Člen | 15
+
0
-

Zdravim, nenapsal si nekdo uz rozsireni puvodniho startup aby dokazal chranit i signaly? momentalne kdyz dany clovek nema pravo jit na dany wiev tak ho to nepusti, ovsem ja ho potrebuju pustit na view a zabranit mu v nekterych signalech :)

Ondřej Mirtes
Člen | 1536
+
0
-

Aktuální signál najdeš v $this->getSignal(). Je to pole, první položka je název komponenty (pokud to je přímo signál presenteru, bude tam NULL) a druhá položka je název signálu.

SquirrelCZE
Člen | 15
+
0
-

dekuju, sice uz jsem to musel zrusit ovsem slouzilo dobre :).
Nicmene ted jsem narazil na dalsi problem, nevim proc ale ve fazi startup() se mi pri tomhle kodu vypise tohle: Array ( [0] ⇒ )

<?php
		$user = Environment::getUser();

		print_r($user->getRoles());

	 	if(!$user->isAllowed($this->name, $this->view))
        {
			$this->redirect('Index:');
        }
?>

pritom predtim to nedelalo a tim padem mi to zpusobuje zacikleni an edokazu se nikam dostat.

Tohle se stava jenom po tom co se pokusim prihlasit (i behem prihlasovani mam prazdne pole).. takze neni mozne se prihlasit a i po nepovedenem prihlaseni zustava pole prazdne :(

Petr Daňa
Člen | 109
+
0
-

Tohle je moc vytržené z kontextu, takže těžko něco radit, ale nedávno jsem řešil něco obdobného, kolega měl chybku v kódu, a sice tu, že ve startup() presenteru volal parent:startup() až na konci, místo na začátku, a protože v rodičovském BasePresenteru ve startupu se řeší kontrola přihlášení, tak ten jeho presenter vlastně nejdřív zkoušel pracovat s rolema, které se ale načetly v reále až potom. Tak jestli to nebudeš mít něco podobného?

SquirrelCZE
Člen | 15
+
0
-

mno, spis jsem zjistil ze chyba byla v DIBI dotazu:) mel jsem ->on(array(‚usr.role_id‘ ⇒ ‚rol.id‘))
coz to neslozilo tak jak sem zamyslel :(

muflon
Člen | 14
+
0
-

Chcel by som sa spytat ked pride navstevnik na stranku a nepresiel prihlasovacim procesom tak $user->getIdentity() vracia NULL ?

Je mozne nejakym sposobom nastavit prava v ACL pre tohto pouzivatela? Pripadne ako nastavit aby neprihlaseny pouzivatel mal priradene role napriklad guest.

Ani
Člen | 226
+
0
-

Nepřihlášený návštěvník by měl mít roly ‚guest‘ automaticky viz https://doc.nette.org/…thentication#…

muflon
Člen | 14
+
0
-

Ani napsal(a):

Nepřihlášený návštěvník by měl mít roly ‚guest‘ automaticky viz https://doc.nette.org/…thentication#…

dakujem velmi si mi pomohol :)