acl - Symfony - Efficient access control for (dynamic) hierarchical roles -
i need advice on how handle access control following scenario:
- corporation
- has 1 or many companies
- has 1 or many role_corp_admin
- company
- has 1 or many regions.
- has 1 or many role_company_admin.
- region:
- has 0 or many stores.
- has 1 or many role_region_admin.
- store:
- has 0 or many assets.
- has 1 or many role_store_admin.
- has 0 or many role_store_employee.
- has 0 or many role_store_customer (many better).
the application should support many corporations.
my instinct create either many-to-many relationship per entity admins (eg region_id
, user_id
). depending on performance, go more denormalized table user_id
, corporation_id
, company_id
, region_id
, , store_id
. i'd create voter class (unanimous strategy):
public function vote(tokeninterface $token, $object, array $attributes) { // if super_admin, return access_granted // if user in $object->getadmins(), return access_granted // else, return access_denied }
since permissions hierarchical, getadmins()
function check owners admins well. instance: $region->getadmins()
return admins owning company, , corporation.
i feel i'm missing obvious. depending on how implement getadmins()
function, approach require @ least 1 hit db every vote. there "better" way go this?
thanks in advance help.
i did posed above, , working well. voter easy implement per symfony cookbook. many-to-many <entity>_owners
tables work fine.
to handle hierarchical permissions, used cascading calls in entities. not elegant, not efficient, not bad in terms of speed. i'm sure refactor use single dql query soon, cascading calls work now:
class store implements ownableinterface { .... /** * @orm\manytomany(targetentity="person") * @orm\jointable(name="stores_owners", * joincolumns={@orm\joincolumn(name="store_id", referencedcolumnname="id", nullable=true)}, * inversejoincolumns={@orm\joincolumn(name="person_id", referencedcolumnname="id")} * ) * * @var arraycollection|person[] */ protected $owners; ... public function __construct() { $this->owners = new arraycollection(); } ... /** * returns people owners of object * @return arraycollection|person[] */ function getowners() { $effectiveowners = new arraycollection(); foreach($this->owners $owner){ $effectiveowners->add($owner); } foreach($this->getregion()->getowners() $owner){ $effectiveowners->add($owner); } return $effectiveowners; } /** * returns true if person owner. * @param person $person * @return boolean */ function isowner(person $person) { return ($this->getowners()->contains($person)); } ... }
the region
entity implement ownableinterface
, getowners()
call getcompany()->getowners()
, etc.
there problems array_merge
if there no owners (null), new $effectiveowners arraycollection
seems work well.
here voter. stole of voter code , ownableinterface
, ownerinterface
knpradbundle:
use acme\acmebundle\security\ownableinterface; use acme\acmebundle\security\ownerinterface; use acme\acmeuserbundle\entity\user; use symfony\component\security\core\authentication\token\tokeninterface; use symfony\component\security\core\authorization\voter\voterinterface; class isownervoter implements voterinterface { const is_owner = 'is_owner'; private $container; public function __construct(\symfony\component\dependencyinjection\containerinterface $container) { $this->container = $container; } public function supportsattribute($attribute) { return self::is_owner === $attribute; } public function supportsclass($class) { if (is_object($class)) { $ref = new \reflectionobject($class); return $ref->implementsinterface('acme\acmebundle\security\ownableinterface'); } return false; } public function vote(tokeninterface $token, $object, array $attributes) { foreach ($attributes $attribute) { if (!$this->supportsattribute($attribute)) { continue; } if (!$this->supportsclass($object)) { return self::access_abstain; } // token super user? check roles, not user. if ( $this->container->get('security.context')->isgranted('role_super_admin') ) { return voterinterface::access_granted; } if (!$token->getuser() instanceof user) { return self::access_abstain; } // check see if token user. if (!$token->getuser()->getperson() instanceof ownerinterface) { return self::access_abstain; } // person owner? if ($this->isowner($token->getuser()->getperson(), $object)) { return self::access_granted; } return self::access_denied; } return self::access_abstain; } private function isowner(ownerinterface $owner, ownableinterface $ownable) { return $ownable->isowner($owner); } }
Comments
Post a Comment