简体   繁体   English

Symfony - (动态)分层角色的高效访问控制

[英]Symfony - Efficient access control for (dynamic) hierarchical roles

I need some advice on how to handle access control for the following scenario: 我需要一些关于如何处理以下场景的访问控制的建议:

  • Corporation 公司
    • Has one or many companies 有一个或多个公司
    • Has one or many ROLE_CORP_ADMIN 有一个或多个ROLE_CORP_ADMIN
  • Company 公司
    • Has one or many regions. 有一个或多个地区。
    • Has one or many ROLE_COMPANY_ADMIN. 有一个或多个ROLE_COMPANY_ADMIN。
  • Region: 区域:
    • Has zero or many stores. 有零或多商店。
    • Has one or many ROLE_REGION_ADMIN. 有一个或多个ROLE_REGION_ADMIN。
  • Store: 商店:
    • Has zero or many assets. 拥有零个或多个资产。
    • Has one or many ROLE_STORE_ADMIN. 有一个或多个ROLE_STORE_ADMIN。
    • Has zero or many ROLE_STORE_EMPLOYEE. 有零个或多个ROLE_STORE_EMPLOYEE。
    • Has zero or many ROLE_STORE_CUSTOMER (many is better). 有零个或多个ROLE_STORE_CUSTOMER(很多更好)。

The application should support many corporations. 该应用程序应该支持许多公司。

My instinct is to create either a many-to-many relationship per entity for their admins (eg region_id , user_id ). 我的直觉是为他们的管理员创建每个实体的多对多关系(例如region_iduser_id )。 Depending on performance, I could go with a more denormalized table with user_id , corporation_id , company_id , region_id , and store_id . 根据性能,我可以使用带有user_idcorporation_idcompany_idregion_idstore_id的更加非规范化的表。 Then I'd create a 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 the permissions are hierarchical, the getAdmins() function will check all owners for admins as well. 由于权限是分层的,因此getAdmins()函数也会检查所有所有者的管理员。 For instance: $region->getAdmins() will also return admins for the owning company, and corporation. 例如: $region->getAdmins()也将返回拥有公司和公司的管理员。

I feel like I'm missing something obvious. 我觉得我错过了一些明显的东西。 Depending on how I implement the getAdmins() function, this approach will require at least one hit to the db every vote. 根据我实现getAdmins()函数的方式,这种方法每次投票都需要至少一次命中数据库。 Is there a "better" way to go about this? 有没有“更好”的方式来解决这个问题?

Thanks in advance for your help. 在此先感谢您的帮助。

I did just what I posed above, and it is working well. 我做了我上面提到的,它运作良好。 The voter was easy to implement per the Symfony cookbook . 根据Symfony食谱 ,选民很容易实施。 The many-to-many <entity>_owners tables work fine. 多对多<entity>_owners表工作正常。

To handle the hierarchical permissions, I used cascading calls in the entities. 为了处理分层权限,我在实体中使用了级联调用。 Not elegant, not efficient, but not to bad in terms of speed. 不优雅,不高效,但速度方面也不差。 I'm sure refactor this to use a single DQL query soon, but cascading calls work for now: 我确定很快就会重构这个,以便使用单个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));
    }

    ...

}

The Region entity would also implement OwnableInterface and its getOwners() would then call getCompany()->getOwners() , etc. Region实体也将实现OwnableInterface然后它的getOwners()将调用getCompany()->getOwners()等。

There were problems with array_merge if there were no owners (null), so the new $effectiveOwners ArrayCollection seems to work well. 如果没有所有者(null),则array_merge存在问题,因此新的$effectiveOwners ArrayCollection似乎运行良好。

Here is the voter. 这是选民。 I stole most of the voter code and OwnableInterface and OwnerInterface from KnpRadBundle : 我偷了大多数选民的代码和OwnableInterfaceOwnerInterfaceKnpRadBundle

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 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);
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM