Best practices: ACL, Autentizaci, autorizaci…
- Roman Ožana
- Člen | 52
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
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ší :-)
- Roman Ožana
- Člen | 52
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
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
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)
- Ondřej Mirtes
- Člen | 1536
Stačí volat
Environment::getUser()->isAllowed($resource, $privilege)
a je to
pořešené podle jeho role…
- jasir
- Člen | 746
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
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
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
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
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
}
}
- PetrP
- Člen | 587
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
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
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
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 | 8218
A co zajistit/vynutit, že bylo voláno parent::startup()
? Na
to by se nějaký obecný mechanismus udělat dal.
- PetrP
- Člen | 587
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 | 8218
Hele mě se to líbí ;)
Mám začít vyhazovat warning, když se nezavolá parent::startup()? Co myslíte? Jsem pro!
- Ondřej Mirtes
- Člen | 1536
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
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
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é:-)
- couda
- Člen | 9
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
Problém bude v abstract class BasePresenter extends Presenter
Zřejmě již takovou Class ve své aplikaci máte
- SquirrelCZE
- Člen | 15
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
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
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
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
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
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
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
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 :)