[2009-01-21] Šikovnější Permission

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
David Grudl
Nette Core | 8107
+
0
-

Mějme jednoduché ACL scéma:

$acl = new Permission;
$acl->addRole('member');
$acl->addResource('article');

Nastavit, že role member má přístup ke zdroji article, je snadné:

$acl->allow('member', 'article');

$acl->isAllowed('member', 'article'); // -> vrací TRUE

Pokud by mělo rozhodování fungovat dynamicky, tedy že member bude mít přístup jen mimo pracovní dobu, lze využít IPermissionAssertion:

class TimeAssertion implements IPermissionAssertion
{
	public function assert(Permission $acl, $role, $resource, $privilege)
	{
		return !Time::isWorkingTime(); // fiktivní metoda
	}
}

$acl->allow('member', 'article', NULL, new TimeAssertion);

$acl->isAllowed('member', 'article'); // o výsledku rozhoduje TimeAssertion::assert()

Jak ale řešit případ, když role member má mít přístup ke zdroji article jen tehdy, pokud je jeho vlastníkem?

Od revize 193 je k tomu možné použít rozhraní IRole a IResource:

class Member implements IRole
{
	public $ownerId;

	public function getRoleId()
	{
		return 'member';
	}
}


class Article implements IResource
{
	public $ownerId;

	public function getResourceId()
	{
		return 'article';
	}
}

$member = new Member;
$article = new Article;
$member->ownerId = $article->ownerId = 123; // vymyslíme nějaké hodnoty ownerId

$acl->allow('member', 'article', NULL, new MyAssertion);

// místo $acl->isAllowed('member', 'article') zavoláme
$acl->isAllowed($member, $article); // skutečný název role a resource vracejí metody getRoleId resp. getResourceId

Zbývá vytvořit nový MyAssertion, který bude porovnávat vlastnosti ownerId předané role a zdroje.

class MyAssertion implements IPermissionAssertion
{
	public function assert(Permission $acl, $role, $resource, $privilege)
	{
		return $acl->getQueriedRole()->ownerId === $acl->getQueriedResource()->ownerId;
	}
}

Proč v metodě není přímo return $role->ownerId === $resource->ownerId? Parametry $role a $resource jsou řetezce, zde konkrétně 'member' a 'article' a to z toho důvodu, že celá struktura ACL může být složitější:

$acl = new Permission;

$acl->addRole('registered');
$acl->addRole('member', 'registered');

$acl->addResource('item');
$acl->addResource('article', 'item');

$acl->allow('registered', 'item', NULL, new MyAssertion); // povolení pro member & article vyplývá z dědičnosti

Pokud nyní zavoláme $acl->isAllowed($member, $article);, bude metodě MyAssertion::assert() jako $role & $resource předány hodnoty 'registered' & 'item', tedy právě ty hodnoty, s jakými byl volán allow(...). Skutečné argumenty, které jsme předali metodě isAllowed(...) se proto získají pomocí metod getQueriedRole() a getQueriedResource().


Třída Permission doznala řady úprav, nicméně by měla zůstat zpětně kompatibilní podle současné dokumentace.

krissott
Člen | 48
+
0
-

Nadhera. Diky Davide.

Tomik
Nette Evangelist | 485
+
0
-

Souhlasím, to je to, co mi chybělo. Díky!

krajaac
Člen | 45
+
0
-

Super, moc dík Davide.

Ještě by se mi hodilo propojení s User.
Nevím, jak to mám implementovat, ale chtěl bych něco takového:

/* Prihlaseny uzivatel, role: admin, v identite: user_id = 123
 * @var $user User
 */
$user;

$article = new Article();
$article->ownerId = 123;

$user->isAllowed($article, 'edit'); // melo by vratit TRUE

ACL schéma vypadá nějak takto:

$acl = new Permission;
$acl->addRole('admin');
$acl->addResource('article');
$acl->allow('admin', 'article', 'edit', new MyAssertion);

Pak v MyAssertion asi něco jako toto:

class MyAssertion implements IPermissionAssertion
{
        public function assert(Permission $acl, $role, $resource, $privilege)
        {
                return $acl->getQueriedUser()->getIdentity()->user_id === $acl->getQueriedResource()->ownerId;
        }
}

Může někdo poradit, jak na to?

Editoval krajaac (23. 1. 2009 14:49)

Ondrej
Člen | 110
+
0
-

krajaac napsal(a):

public function assert(Permission $acl, $role, $resource, $privilege)
{
return $acl->getQueriedUser()->getIdentity()->user_id === $acl->getQueriedResource()->ownerId;
 }
}

A co to udelat takto?

<?php
  public function assert(Permission $acl, $role, $resource, $privilege)
  {
    return Environment::getUser()->getIdentity()->user_id === $acl->getQueriedResource()->ownerId;
  }
?>
krajaac
Člen | 45
+
0
-

Ondrej napsal(a):

A co to udelat takto?

<?php
  public function assert(Permission $acl, $role, $resource, $privilege)
  {
    return Environment::getUser()->getIdentity()->user_id === $acl->getQueriedResource()->ownerId;
  }
?>

Dík, to by šlo.. Pořád jsem hledal složité řešení, až jsem si neuvědomil, že to jde tak jednoduše :)

Jod
Člen | 701
+
0
-

Niečo takéto asi pomocou anotácie implementovať nepôjde, čo :D

Jod
Člen | 701
+
0
-

Jod napsal(a):

Niečo takéto asi pomocou anotácie implementovať nepôjde, čo :D

Mohlo by to ísť tak, že by sa vlastník zdroja zistil už v actionX a práva pomocou anotácie kontrolovali v renderXY.

Jod
Člen | 701
+
0
-

Ako môžem takéto assertion aplikovať na zoznamy? Pretože aj keď môžem riešiť prístup k jednému prvku, ale jemu zvykne predchádzať celý zoznam a tam mi nejak nenapadá ako nato.

actionDefault → Zoznam článkov. Podľa práv vidí všetky, alebo len svoje. To je najlepšie ošetriť where podmienkov už v sql. Ako zistiť, že tam mám hodiť tú podmienku?
actionDetail → Detail článku. Tu je to jasné z príkladu.

Vie niekto? Ešte si začnem myslieť, že nette neni dokonalé =D

Jod
Člen | 701
+
0
-

No ok, vyriešené. Aj keď asi nič moc.

viktorc
Člen | 21
+
0
-

Ked implementujem vlastné Member a Article a vrátim po autentizácii v Identity objekt Member, konstructor Identity ho prevedie na array. Mohol by zostat naďalej ako objekt Member (IRole)?
Dal by sa potom elegantnejšie overovať prístup. Bolo by to také čistejšie.

// MyAuthenticator po uspesnom overeni uzivatel vrati Role
function MyAuthenticator::authenticate (..) {
	...
	return new Identity($login, new Member($ownerId), $userdata);
}

function MyAssertion::assert(Permission $acl, $role, $resource, $privilege) {
	$acl->getQueriedRole() ....
	...
}

// nasledne overenie autorizacie v Presenteri, vyvola MyAssertion::assert,
// ale k objektu Member sa cez getQueriedRole() nedostanem
Environment::getUser()->isAllowed(new Article($ownerId))

// iste, da sa to nahradit, komplikovanejsim
$identity=Environment::getUser()->getIdentity();
$member=new Member($identity->role, $identity->ownerId);
$acl=Environment::getUser()->getAuthorizationHandler();
$acl->isAllowed($member, new Article($ownerId))

Editoval viktorc (14. 8. 2009 15:44)

vlki
Člen | 218
+
0
-

Konstruktor Identity totiž počítá s tím, když má nějaký uživatel více rolí. Mělo by tedy stačit změnit to vytváření identity na:

// MyAuthenticator po uspesnem overeni uzivatel vrati Role
MyAuthenticator::authenticate (..) {
        ...
        return new Identity($login, array(new Member($ownerId)), $userdata);
}

Zkus;)

Editoval vlki (14. 8. 2009 15:32)

viktorc
Člen | 21
+
0
-

Super. Presne to som potreboval.
Funguje to, diky.

phx
Člen | 651
+
0
-

Otazka: Proc Assertion musi byt objekt? Osobne se mi jevi hezci a univerzalnejsi callback.

David Grudl
Nette Core | 8107
+
0
-

phx napsal(a):

Otazka: Proc Assertion musi byt objekt? Osobne se mi jevi hezci a univerzalnejsi callback.

Odpověď je trošku rozsáhlejší. Ale ano, tady by callback nejspíš pasoval lépe.

Roman Ožana
Člen | 52
+
0
-

Dneska jsem experimentoval s tímhle vypadá to aj celkem použitelně.

Mám obecnou třídu implementující IResource

<?php
class Resource implements IResource {
  public $id = null;
  public $name = null;
  function  __construct($name , $id = null) {
    $this->name = $name;
    $this->id = $id;
  }
  public function getResourceId() {
    return $this->name;
  }
}
?>

Pak mám obecnou třídu implementující IRole:

<?php
class Role implements IRole {
  public $id = null;
  const guest  = 'guest';
  const member = 'member';
  const admin  = 'admin';
  public $role = null;
  function  __construct() {
    $args = func_get_args(); // get input args
    // instance of User or Identity
    if (func_num_args() == 1) {
      if ($args[0] instanceof User) {
        $this->role =  $args[0]->getIdentity()->role;
        $this->id = $args[0]->getIdentity()->id;
      } elseif ($args[0] instanceof Identity) {
        $this->role = $args[0]->role;
        $this->id = $args[0]->id;
      } else {
        throw new Exception('Wrong input object.');
      }
    }
    // role / id
    if (func_num_args() >= 2) {
      $this->role = $args[0];
      $this->id = $args[1];
    }
  }
  public function getRoleId() {
    return $this->role;
  }
}
?>

a samozřejmě taky

<?php
class PermissionAssertion implements IPermissionAssertion {
  public function assert(Permission $acl, $role, $resource, $privilege) {
    if ($role === Role::admin) return true; // admin can everything
    // if ($role == Member::role) return Environment::getUser()->getIdentity()->id === $acl->getQueriedResource()->id; // member can only self account
    return $acl->getQueriedRole()->id === $acl->getQueriedResource()->id;
    //return false;
  }
}
?>

No a nakonec zhruba takovéhle Permission

<?php
class ACL extends Permission {
  public function __construct() {
    $this->addRole(Role::guest);               // guest
    $this->addRole(Role::member, Role::guest); // member
    $this->addRole(Role::admin, Role::member); // admin
    $this->addResource('account');
    $this->deny(Role::guest);
    $this->allow(Role::member, 'account', null, new PermissionAssertion());
    $this->allow(Role::admin, 'account', null, new PermissionAssertion());
  }
}
?>

Potom stačí napsat:

<?php
$acl = new ACL();
Debug::dump($acl->isAllowed(new Role(Environment::getUser()), new Resource('account', 71))); // zalezi na id uzivatele
Debug::dump($acl->isAllowed(new Role('admin', 71), new Resource('account', 71))); // vrací vzdy true
Debug::dump($acl->isAllowed(new Role('guest', 71), new Resource('account', 71))); // vrací false
// atd.
?>

Editoval Roman Ožana (19. 1. 2010 15:28)

Teyras
Člen | 81
+
0
-

Můžu se zeptat, proč už tahle feature není v Nette 1.0? Nebo se to změnilo?

hrach
Člen | 1834
+
0
-

je tam, jen misto obejktu predavas callbacky, viz https://github.com/…a74f1468935c

Teyras
Člen | 81
+
0
-

hrach napsal(a):

je tam, jen misto obejktu predavas callbacky, viz https://github.com/…a74f1468935c

OK, to je určitě o něco intuitivnější…

brano
Člen | 25
+
0
-

Mohli by ste prosim niekto poslat ukazku ako to funguje s callbackmi? Nejak sa mi to nedari nastudovat. Dik.

brano
Člen | 25
+
0
-

Tusim toto zozerie:

$this->allow('member','article', 'edit', new Nette\Callback( new Article_Edit_Assertion, assert ) );

Editoval brano (20. 7. 2010 18:05)

bojovyletoun
Člen | 667
+
0
-

Ahoj, chystám se použít Permission a to tak, aby měl k editaci článku(a jeho potomkům- článek je i kategorie) přístup ten, kdo ho vytvořil. Jedna cesta by byla, že co uživatel, to role. To se mi zdá nečisté. Proto bych vytvořil jen roli editor a množství resource [„article12“,„article732“].
Je dobré pro to využít asserce? Nebo nějakou jednodušší třídu (jenže budu chtít Authorizator využít i na jiné věci)
V aserci by se kouklo, zda id aktuálního uživatele je id autora resource.

Dál se chci zeptat na další informace ohledě Assercí- téma je staré, v dokumentaci jsem toho moc nenašel a ve fóru 2 topicy.

Jinak chápu to správně: queriedrole/resource se používá kvůli tomu, že interně se pracuje se stringy (v případě že role/zdroje jsou objekty)?

Aurielle
Člen | 1281
+
0
-

Asserce byly nahrazeny callbacky.

Filip Procházka
Moderator | 4668
+
0
-

A proč by uživatel nemohl být role? Co takhle mít role, ale do toho ještě uživatele jako role. Tím můžeš mít skupiny oprávnění (role) a navíc specifické oprávnění (uživatelé).

bojovyletoun
Člen | 667
+
0
-

Vlastně, když to rozmýšlím, tak je to asi jediná cesta a není ani nečisté. Prostě mě by se hodilo, aby Permission nebral role, ale rovnou uživatele. A právě tím, že definuji roli za každého uživatele, tak tím docílím takového chování. Samozřejmě klasické role (generic, skupiny) s tím lze míchat.

gmvasek: chtěl bych se o tom dozvědět víc. Původně jsem sem chtěl napsat, že jediné, co jsem se dočetl, je, že místo class Assertion jsou callbacky.

Jinak ještě jsem našel další pohled na Asserce (i když do jsou funkce už, tak se mi tenhle název líbí): defatko umožňuje seskupovat množiny zdrojů – místo, abych definoval pro každý article jeden resource, tak definuji 1 article a v asserci rozhoduji, zda je user jeho vlastníkem. Šikovná věc.

Aurielle
Člen | 1281
+
0
-

Prostě aserce při volání isAllowed (v případě, že je definována) může rozhodnout o povolení/zamítnutí přístupu. (Víc nevím, netestoval jsem)

Podbor
Člen | 19
+
0
-

Zdravím,
já bych měl k tomuto tématu otázku na odmítnutí či povolení přístupu k jednotlivým částem aplikace. Práva mám definována v databázi dle schématu ACL (resource, privilege, role, acl) a takto je načítám přes svůj Autorizátor, který dědí třídu „Permission“.
Chtěl bych vyřešit to, že když uživatel přes URL zadá požadavek na nějaký presenter a akci, ke které nemá přístup, aby byl přesměrován na hlavní stránku. V současné době jsem to vyřešil takhle:

<?php
abstract class BasePresenter extends Nette\Application\Presenter
{

      protected function startup()
      {
        parent::startup();
        if(!$this->getUser()->isAllowed($this->getResource(),'show')) $this->redirect('Homepage:');//pokud uživatel nemá právo prohlížet daný Presenter, bude automaticky přesměrován
      }//public function startup()

      /**
       * Chráněná abstraktní metoda "getResource", kteoru mís všichni potomci Základního preseneru překrýt. Má vracet řetězec obsahující název "zdroje"
       */
      protected abstract function getResource();

?>

Čili předek všech Presenterů kontroluje přes „isAllowed“, jestli uživatel má právo na daný zdroj. (Zatím to zkouším pouze na úrovni toho, že beru zdroj = presenter). V každém potomkovi pak dojde k implementaci metody „getResource()“, která vrací řetězec se jménem presenteru.

<?php
class RegistrationPresenter extends BasePresenter
{
  private $resource = "registration";

  protected function getResource()
  {
    return $this->resource;
  }//protected function getResource()
?>

A přesně tady bych se chtěl zeptat, jestli to jde vyřešit nějak lépe než prostě staticky uloženou hodnotou ve vnitřní proměnné daného presenteru?
Předem díky za rady.

bojovyletoun
Člen | 667
+
0
-

využít` $this->name nebo $this->action `.. jinak doporučuji pročíst toto , je to ale dlouhé, ale pořád jde o jeden z nejlepších tutoriálů

Editoval bojovyletoun (23. 3. 2011 15:02)

Podbor
Člen | 19
+
0
-

bojovyletoun napsal(a):

využít` $this->name nebo $this->action `.. jinak doporučuji pročíst toto , je to ale dlouhé, ale pořád jde o jeden z nejlepších tutoriálů

Díky Ti, to první je přesně to, co jsem hledal, ale nějak jsem se k těmto vlastnostem nemohl dopátrat. Na ten zmíněný odkaz se ještě určitě podívám, už jsem si ho jednou četl, ale ještě to jednou zopakuji, ať se konečně dostanu k tomu „jádru pudla“.

Editoval Podbor (24. 3. 2011 9:25)

OndrejMalacka
Člen | 2
+
0
-

Chtěl bych se zeptat, jakým způsobem je možné šikovnější permissions použít v šabloně (kvůli zobrazení/nezobrazení ikon). Pokusil sem se (neúspěšně) to udělat následovně:

Šablona:

	{if $user->isAllowed(new UsersResource($u['id']),'edit')}
	    <a href="{plink edit,$u->id}">
		<img src="{$baseUri}/images/editIcon.gif" alt=""/>
	    </a>
	{/if}

Třída resource:

class UsersResource implements Nette\Security\IResource {

    public $ownerId;

    function __construct($id) {
        $this->ownerId = $id;
    }

    public function getResourceId() {
        return 'Front:Users';
    }
}

co se tyce ACL:

 $this->allow('user', 'Front:Users', 'edit', function($permission, $role, $resource, $privilege) {
                    return Nette\Environment::getUser()->getIdentity()->id === 		$permission->getQueriedResource()->ownerId;
});

Když se to pokusím udělat v presenteru, tak to funguje jak má, ovšem v šabloně už nikoliv …

newPOPE
Člen | 648
+
0
-

V sablone new to by som tam moc netlacil. Skor to co je v $u by malo iplementovat IResource.

Ale preco to v sablone nende moc netusim… asi by malo

OndrejMalacka
Člen | 2
+
0
-

Tak potřebuji projít v šabloně přes všechny položky a editační tlačítko u té položky, která je daného uživatele. Sice by to šlo obejít, ale chtěl sem to mít hezky právě pomocí šikovnějších permissions

llook
Člen | 407
+
0
-

Trochu se vracím k programování a občas se zasekávám na základních věcech. Teď přemýšlím, jak nejelegantněji řešit velmi obvyklý úkol: Přihlášený uživatel může upravovat svůj profil, ale ne ostatní profily.

Takže mám uživatele $user a nějaký uživatelský profil $profile. Jak má vypadat ta omáčka, aby se dalo napsat toto?:

$user->isAllowed($profile, 'edit');

Autorizér pro rozhodnutí potřebuje znát $user->id a $profile->user_id. Přečetl jsem si toto vlákno a zatím nejpoužitelnější, co mi z toho vychází, je zhruba tohle:

class User extends Nette\Security\User implements Nette\Security\IRole
{
	public function getRoleId()
	{
		return __CLASS__;
	}

	public function __construct()
	{
		$this->addRole($this);
	}
}

class Profile implements Nette\Security\IResource
{
	// ...
}



$acl->allow(null, null, 'edit', function ($permission, $role, $resource, $privilege) {
	$queRole = $permission->getQueriedRole();
	$queResource = $permission->getQueriedResource();
	if (($queRole instanceof User) && ($queResource instanceof Profile)) {
		return $queRole->getId() === $queResource->getUserId();
	} else {
		return false;
	}
});

Zkrátka z uživatele udělat jeho vlastní roli a jeho ID potom získávat přes getQueriedRole.

Ale moc se mi to nelíbí. Kdybych to tímto způsobem psal v C#, musel bych přetypovávat a to obvykle spolehlivě značí špatný návrh. Krom toho si nejsem moc jistý tím, co mám vracet v getRoleId a v getResourceId – spíš nějaký identifikátor typu („profile“, „user“ apod.) nebo spíš identifikátor konkrétního objektu („profile/123“, „user/321“ apod.)?

Budu rád za každou myšlenku.

awsickness
Člen | 98
+
0
-

mzues to mit udelane takhle

mas objekt user nad kterym udelas neco jako isEditable nebo tak neco a tam se podivas zda sedi tento uzivatel s uzivatelem ktery si na zaklade id vytahl z db.
prijde mi to nejrozumnejsi reseni, ktery jde udelat.

$user->canEdit($user->identity)
....
llook
Člen | 407
+
0
-

Profilem jsem nemyslel systémové údaje o uživateli, ale spíš něco jako profil na Facebooku nebo na Lidech. Prostě stránka, kam si uživatel může nahrát fotku a nějaký kecy, ale jinak je to samostatná entita.

Uznávám, že jsem možná nevybral nejlepší příklad, ale můžeme si místo profilu představit třeba článek v blogu: Uživatel může upravovat svoje články, ale ne cizí.

class User extends Nette\Security\User implements Nette\Security\IRole
{
	function __construct()
	{
		$this->addRole($this);
	}
	...
}

class Article implements Nette\Security\IResource
{ ... }

$acl->allow(null, null, 'edit', function ($permission, $role, $resource, $privilege) {
        $queRole = $permission->getQueriedRole();
        $queResource = $permission->getQueriedResource();
        if (($queRole instanceof User) && ($queResource instanceof Article)) {
                return $queRole->getId() === $queResource->getAuthorId();
        } else {
                return false;
        }
});

Příklad použití:

{if $user->isAllowed($article, 'edit')}
	<a n:href="Article:edit $article->id">Překopat</a>
{/if}

Je tohle ten nejlepší způsob?

Jan Jakeš
Člen | 177
+
0
-

Ehm, četl si první první příspěvek? Zdá se mi to nebo popisuje přesně to, co potřebuješ? Nejčistším řešením asi bude použití Assertion.

Aurielle
Člen | 1281
+
0
-

Assertion už hodně dlouho neexistuje.

Filip Procházka
Moderator | 4668
+
0
-

Ale callback (nebo closuru) to přijímá pořád.

llook
Člen | 407
+
0
-

Juan napsal(a):

Ehm, četl si první první příspěvek? Zdá se mi to nebo popisuje přesně to, co potřebuješ? Nejčistším řešením asi bude použití Assertion.

Ehm, četl. Ano, první příspěvek zřejmě navrhuje velmi podobné řešení a mě to připadá jako špinavé řešení. Další příspěvky ve vlákně navrhují buďto totéž, nebo ještě horší (Environment::getUser()). Vzhledem k tomu, že je to 3 roky starý materiál, tak jsem doufal, že někdo od té doby třeba přišel na nějaké čistší řešení.

Jan Tvrdík
Nette guru | 2595
+
0
-

Jestli ti to připadá špinavé, tak máš pravdu. Snažil jsem to HosipLanovi na Nette Brain Cloud vysvětlit, ale pak se to nějak zakecalo. Související vlákno: https://forum.nette.org/…ou-indentitu

Editoval Jan Tvrdík (17. 1. 2012 13:42)