Nalezena chyba v autorizátoru – ACL

Bulldog
Člen | 110
+
0
-

Ahoj, pro testování mám následující autorizátor:

<?php declare(strict_types=1);

namespace App\Model\Security;

use Nette\Security\Permission;

class AuthorizatorFactory
{
    public static function create(): Permission
    {
        $acl = new Permission();

        $acl->addRole('guest');
        $acl->addRole('client', 'guest');
        $acl->addRole('admin', 'client');

        $acl->addResource('Address');

        $acl->deny('guest');

        $acl->allow('client', 'Address', 'add');

        $acl->deny('admin');
        $acl->allow('admin', Permission::ALL, ['view', 'detail']);

        bdump($acl->isAllowed('admin', 'Address', 'add'));	// Vypíše true...

        return $acl;
    }
}

Očividně tedy zavolání funkce deny bez dalších parametrů nefunguje jak by měla. Podle prototypu funkce deny bych očekával, že bude zakazovat všechny privileges nad všemi resources, což se očividně neděje.

EDIT

"php": ">=8.1",
"nette/application": "^3.1",
"nette/bootstrap": "^3.1",
"nette/caching": "^3.1",
"nette/database": "^3.1",
"nette/di": "^3.0",
"nette/finder": "^2.5",
"nette/forms": "^3.1",
"nette/http": "^3.1",
"nette/mail": "^3.1",
"nette/robot-loader": "^3.3",
"nette/security": "^3.1",
"nette/utils": "^3.2",
"latte/latte": "^2.9",
"tracy/tracy": "^2.8",

Editoval Bulldog (2. 11. 2022 20:45)

Bulldog
Člen | 110
+
0
-

Tak ještě update, pokud udělám toto:

$acl->allow('client', 'Address', 'add');

$acl->deny('admin', 'Address');		// Přidána resource
$acl->allow('admin', Permission::ALL, ['view', 'detail']);

bdump($acl->isAllowed('admin', 'Address', 'add'));	// Už vypíše false.

Tak už se add nepodědí a opravdu se přístup k Address zakáže.
Pokud se ale zeptám na to view, nebo detail, který následně povoluji všem:

$acl->allow('client', 'Address', 'add');

$acl->deny('admin', 'Address');		// Přidána resource
$acl->allow('admin', Permission::ALL, ['view', 'detail']);

bdump($acl->isAllowed('admin', 'Address', 'view'));	// Vypíše false.
bdump($acl->isAllowed('admin', 'Address', 'detail'));	// Vypíše false.

Tak vypíše false, i když jsem jej povolil na všechny resources, tedy i na Address

Editoval Bulldog (2. 11. 2022 20:50)

Šaman
Člen | 2658
+
0
-

Chápu to tak, že konkrétní oprávnění má přednost před konstantou ::ALL.
Takže třeba v tom posledním příkladu má admin práva view a detail na cokoliv, ale zároveň má zakázané všechno nad Address a toto pravidlo má vyšší prioritu.

Není to sekvenční, práva na ::ALL se vztahují i na zdroje a oprávnění přidané později. Takže ve chvíli, kdy voláš metodu allow, tak se mu nenastaví přístup ke konkrétním zdrojům, ale uloží se u něho informace, že jedno z pravidel je „přístup na ALL“. A teprve až se zavolá isAllowed, tak se projdou všechna pravidla a vyhodnotí se. Skoro bych i řekl, že je jedno v jakém pořadí se práva přidělují/zakazují.

Editoval Šaman (2. 11. 2022 22:34)

Bulldog
Člen | 110
+
0
-

Kdyby to tak bylo, tak bych to chápal taky.
Ovšem v dokumentaci toto chování není popsáno, jediné, co tam je jen ohledně rolí.
Role může dědit od jiné role či od více rolí. Co se ale stane, pokud má jeden předek akci zakázanou a druhý povolenou? Jaké budou práva potomka? Určuje se to podle váhy role – poslední uvedená role v seznamu předků má největší váhu, první uvedená role tu nejmenší.

Bohužel to, že by konkrétní pravidlo mělo přednost před obecným ::ALL nesplňuje tento fakt:

$acl->allow('client', 'Address', 'add');
$acl->deny('client', 'Address', Permission::ALL);

bdump($acl->isAllowed('client', 'Address', 'add'));

Vypíše podle očekávání false.
Přijde mi divné, aby u resources měly konkrétní resources větší váhu, než obecné all a u privileges měly zase váhu podle pořadí.

Editoval Bulldog (3. 11. 2022 9:11)

Bulldog
Člen | 110
+
0
-

Navíc bych i podle té věty poslední uvedená role v seznamu předků má největší váhu předpokládal, že pokud ze sebe role dědí, tak že pravidla pro roli potomka mají větší váhu, než pravidla od předka.

Editoval Bulldog (3. 11. 2022 10:31)

David Grudl
Nette Core | 8217
+
0
-

Na pořadí volání allow() / deny() myslím nezáleží. To že má největší váhu poslední role/resource v seznamu předků se týká pole rolí/resources, které jsou předané jako druhý parametr do addRole/addResource.

Bulldog
Člen | 110
+
0
-

Dobře děkuju.

předělal jsem to, aby to snad fungovalo.

Nebo jak by jste prosím zapsali, že ten, kdo je role supervisor může dělat vše, co ten, kdo je role user, ale nesmí nic editovat?

Původně jsem to měl takto:

$acl->addRole('user');
$acl->addRole('supervisor', 'user');

$acl->deny('user');	// zakážu vše, abych nezapomněl na zákaz čehokoliv.
// pak povoluju jen to, co user může
$acl->allow('user', 'Article', 'add');
$acl->allow('user', 'Article', 'view');
$acl->allow('user', 'Article', 'edit');
$acl->allow('user', 'Article', 'remove');
// další definice pro usera

// a teď nadefinuju supervisora, který může dělat všechno, co user, ale nesmí nic editovat.
$acl->deny('supervisor', Permission::ALL, 'edit');

To ale podle tohoto vlákna nefunguje, tak jsem to musel předělat tak, že supervisor od usera nedědí, ale explicitně pomocí CopyPasty definuju ty pravidla znovu, což je redundance jako blázen. Dá se to nějak zkrátit?

EDIT
Vím, že místo role můžu předat pole rolí, ale mám ve zvyku definovat pravidla pro jednotlivé role zvlášť. Je to kvůli přehlednosti, že vím hned, že definice všeho pro usera mám pohromadě a nemusím to hledat. Tedy pokud bych dělal průniky a dělil to podle nich, tak by musely vznikat další dělení, kde při více než 3 rolích bych se z toho zbláznil, jelikož pro 4 role by bylo až 15 různých částí definic, kde bych stejně musel procházet jednotlivé definice a hledat, co vlastně user může. bych se zbláznil.

Editoval Bulldog (3. 11. 2022 18:58)

David Grudl
Nette Core | 8217
+
0
-

Permission je založeno na Zend_Acl a vnitřně funguje úplně stejně. Takže můžeš jako informační zdroj brát i dokumentaci Zend potažmo Laminas. Jen mám pocit, že ty dokumentace jsou sice obsáhlé, ale úplně plytké.

Každopádně jestli najdeš lepší vysvětlení, jak to přesně funguje, bude skvělé, když to sem odkážeš nebo rovnou přidáš do dokumentace.

Bulldog
Člen | 110
+
0
-

Pecka projdu to díky. :)

Bulldog
Člen | 110
+
0
-

Tak jsem se na to díval a nechovají se stejně.
Viz příklady:

private function laminas(): void
{
    $acl = new \Laminas\Permissions\Acl\Acl();

    $acl->addRole('client');
    $acl->addRole('admin', 'client');

    $acl->addResource('Address');

    $acl->allow('client', 'Address', 'add');
    $acl->deny('admin');

    bdump($acl->isAllowed('admin', 'Address', 'add'), 'Laminas');	// Vypíše false
}

private function zend(): void
{
    $acl = new \Zend\Permissions\Acl\Acl();

    $acl->addRole('client');
    $acl->addRole('admin', 'client');

    $acl->addResource('Address');

    $acl->allow('client', 'Address', 'add');
    $acl->deny('admin');

    bdump($acl->isAllowed('admin', 'Address', 'add'), 'Zend');	// Vypíše false
}

private function nette(): void
{
    $acl = new Nette\Security\Permission();

    $acl->addRole('client');
    $acl->addRole('admin', 'client');

    $acl->addResource('Address');

    $acl->allow('client', 'Address', 'add');
    $acl->deny('admin');

    bdump($acl->isAllowed('admin', 'Address', 'add'), 'Nette');	// Vypíše true
}

Tedy Zend a následně tedy Laminas se chovají podle očekávání a následující pravidlo přepíše předchozí a vypíší správně False, ale Nette jediné to ignoruje a vypíše True.

Zend se dokonce chová tak, že pokud má potomek nastaveno

$acl->deny('admin');

Tak se při isAllowed nad adminem vůbec do předků nekouká a chová se jako by předky neměl.
Podobně se to chová, pokud nastavíme následující:

// Zend:
$acl->deny('admin', null, 'add');
$acl->allow('client', 'Address', 'add');
$acl->allow('client', 'Address', 'edit');

bdump($acl->isAllowed('admin', 'Address', 'add'), 'Zend');	// Vypíše false
bdump($acl->isAllowed('admin', 'Address', 'edit'), 'Zend');	// Vypíše true

// Nette
$acl->deny('admin', null, 'add');
$acl->allow('client', 'Address', 'add');
$acl->allow('client', 'Address', 'edit');

bdump($acl->isAllowed('admin', 'Address', 'add'), 'Zend');	// Vypíše true
bdump($acl->isAllowed('admin', 'Address', 'edit'), 'Zend');	// Vypíše true

Tedy pokud u Zendu obecně nastavíme potomkovi, že nesmí nad všemi resourcemi provádět určité akce, tak při dotazu na danou akci se pravidla předků ignorují.
U Nette ale převládají pravidla předka.

U definic v rámci jedné role pak samozřejmě záleží na pořadí:

// Zend 1
$acl->deny('admin', null, 'add');
$acl->allow('admin', 'Address', 'add');

bdump($acl->isAllowed('admin', 'Address', 'add'), 'Zend');	// Vypíše true

// Zend 2
$acl->allow('admin', 'Address', 'add');
$acl->deny('admin', null, 'add');

bdump($acl->isAllowed('admin', 'Address', 'add'), 'Zend');	// Vypíše false

// Nette 1
$acl->deny('admin', null, 'add');
$acl->allow('admin', 'Address', 'add');

bdump($acl->isAllowed('admin', 'Address', 'add'), 'Nette');	// Vypíše true

// Nette 2
$acl->allow('admin', 'Address', 'add');
$acl->deny('admin', null, 'add');

bdump($acl->isAllowed('admin', 'Address', 'add'), 'Nette');	// Vypíše true

Takže jak vidíme, tak v Zendu na pořadí záleží, zatímco Nette jej ignoruje a v Zendu existuje mnou předpokládaná hierarchie:

  1. Definice v potomkovi mají větší váhu, než definice rodičů.
  2. Pořadí uvedení rodičů určuje váhu definic. Tedy v případě $acl->addRole('admin', ['user', 'guest']) mají definice přímo v roli admin nejvyšší váhu, v roli user nižší a v roli guest nejnižší.
  3. Definice v rámci jedné role mají váhu podle pořadí přidání. Tedy záleží na pořadí volání allow a deny, přičemž pozdější volaný má větší váhu.

Editoval Bulldog (4. 11. 2022 15:02)

Bulldog
Člen | 110
+
0
-

Ptám se tedy co s tím?
Mám použít radši ten Zend/Laminas, nebo mám opravit NetteAcl? Případně ho napsat znova?