Symfony - эффективный контроль доступа для (динамических) иерархических ролей

Мне нужен совет по управлению доступом для следующего сценария:

  • корпорация
    • Имеет ли одна или несколько компаний
    • Имеет один или несколько ROLE_CORP_ADMIN
  • Компания
    • Имеет один или несколько регионов.
    • Имеет один или несколько ROLE_COMPANY_ADMIN.
  • Область:
    • Имеет ноль или много магазинов.
    • Имеет один или несколько ROLE_REGION_ADMIN.
  • Хранить:
    • Имеет нулевые или много активов.
    • Имеет один или несколько ROLE_STORE_ADMIN.
    • Имеет ноль или много ROLE_STORE_EMPLOYEE.
    • Имеет ноль или много ROLE_STORE_CUSTOMER (многие лучше).

Приложение должно поддерживать многие корпорации.

Мой инстинкт заключается в том, чтобы создать для многих region_id (например, region_id, user_id) отношение "много-ко-многим" для каждого объекта. В зависимости от производительности я мог бы пойти с более денормализованной таблицей с user_id, corporation_id, company_id, region_id и store_id. Тогда я бы создал класс избирателей (единодушная стратегия):

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
}

Поскольку разрешения являются иерархическими, getAdmins() будет проверять всех владельцев для администраторов. Например: $region->getAdmins() также вернет администраторов для владеющей компании и корпорации.

Я чувствую, что мне не хватает чего-то очевидного. В зависимости от того, как я реализую getAdmins(), для этого подхода потребуется по крайней мере один удар по db на каждый голос. Есть ли "лучший" способ сделать это?

Заранее спасибо за вашу помощь.

1 ответ

Я сделал то, что я поставил выше, и он работает хорошо. Избирателей было легко реализовать в кулинарной книге Symfony. Таблицы "многие-ко-многим" <entity>_owners</entity> работают нормально.

Для обработки иерархических разрешений я использовал каскадные вызовы в сущности. Не элегантный, не эффективный, но не плохой с точки зрения скорости. Я уверен, что реорганизовать это, чтобы использовать один запрос DQL в ближайшее время, но каскадные вызовы работают сейчас:

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 all people who are owners of the object
 * @return ArrayCollection|Person[]
 */
 function getOwners()
 {
 $effectiveOwners = new ArrayCollection();

 foreach($this->owners as $owner){
 $effectiveOwners->add($owner);
 }

 foreach($this->getRegion()->getOwners() as $owner){
 $effectiveOwners->add($owner);
 }

 return $effectiveOwners;
 }

 /**
 * Returns true if the person is an owner.
 * @param Person $person
 * @return boolean
 */
 function isOwner(Person $person)
 {
 return ($this->getOwners()->contains($person));
 }

 ...

}

Субъект Region также будет реализовывать OwnableInterface и его getOwners() затем вызовет getCompany()->getOwners() и т.д.

Были проблемы с array_merge если не было владельцев (null), поэтому новый $effectiveOwners ArrayCollection похоже, работает хорошо.

Вот избиратель. Я украл большую часть кода избирателя и 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\*******************\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 as $attribute) {

 if (!$this->supportsAttribute($attribute)) {
 continue;
 }

 if (!$this->supportsClass($object)) {
 return self::ACCESS_ABSTAIN;
 }

 // Is the token a super user? This will 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 to see if this token is a user.
 if (!$token->getUser()->getPerson() instanceof OwnerInterface) {
 return self::ACCESS_ABSTAIN;
 }

 // Is this person an 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);
 }
}

licensed under cc by-sa 3.0 with attribution.