symfony2 acl группы

Обычно у меня следующая бизнес-модель:

Есть пользователи и группы. Каждый пользователь принадлежит только к одной группе, и количество групп заранее не определяется (как и количество пользователей для большинства сайтов). Также существует несколько различных объектов занятости, которые могут принадлежать пользователю.

Группы не являются отдельными объектами, которые должны контролироваться самими ACL, но они должны влиять на то, как должны управляться другие объекты, так же, как группы unix.

Есть 3 основные роли: SUPERADMIN, ADMIN и USER.

  • SUPERADMIN может делать что угодно с любой сущностью.
  • ПОЛЬЗОВАТЕЛЬ обычно может читать / писать собственные объекты (включая себя) и читать объекты из своей группы.
  • ADMIN должен иметь полный контроль над объектами в своей группе, но не над другими группами. Я не понимаю, как применить здесь наследование ACL (и можно ли это вообще применить).

Также меня интересует, как запретить доступ можно применить в ACL. Как и у пользователя, есть доступ на чтение / запись ко всем своим полям, кроме логина. Пользователь должен читать только свой логин. Т.е. логично предоставить доступ для чтения / записи к его собственному профилю, но запретить запись для входа в систему, вместо того, чтобы напрямую определять доступ для чтения / записи ко всем его полям (кроме входа в систему).


person kirilloid    schedule 27.04.2012    source источник
comment
Хорошо, я решил это без использования ACL, но с возможностью интеграции ACL: я зарегистрировал свою собственную службу голосования.   -  person kirilloid    schedule 28.04.2012
comment
@krilloid - у меня такой же вопрос, как и у вас. Сможете ли вы поделиться своим кодом услуги избирателя? Это будет высоко ценится. Благодарность   -  person Flukey    schedule 22.05.2012
comment
kirilloid Если вы нашли подходящий дизайн, было бы неплохо разместить его здесь, ответив на свой вопрос. Как и @Flukey, я был бы признателен, так как подхожу к аналогичной задаче. Спасибо.   -  person mokagio    schedule 08.08.2012
comment
@Flukey Я отправил свой код в ответ.   -  person kirilloid    schedule 21.08.2012


Ответы (2)


Хорошо, вот оно. Код не идеален, но это лучше, чем ничего.

Избирательная служба.

<?php
namespace Acme\AcmeBundle\Services\Security;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;

class GroupedConcernVoter implements VoterInterface {

    public function __construct(ContainerInterface $container)
    {   
        $this->container = $container;
        $rc = $this->container->getParameter('grouped_concern_voter.config');
        // some config normalization performed
        $this->rightsConfig = $rc;
    }   

    // even though supportsAttribute and supportsClass methods are required by interface,
    // services that I saw, leaves them empty and do not use them

    public function supportsAttribute($attribute)
    {   
        return in_array($attribute, array('OWNER', 'MASTER', 'OPERATOR', 'VIEW', 'EDIT', 'CREATE', 'DELETE', 'UNDELETE', 'DEPLOY'))
            // hacky way to support per-attribute edit and even view rights.
            or preg_match("/^(EDIT|VIEW)(_[A-Z]+)+$/", $attribute);
    }           

    public function supportsClass($object)
    {   
        $object = $object instanceof ObjectIdentity ? $object->getType() : $object;
        // all our business object, which should be manageable by that code have common basic class.
        // Actually it is a decorator over Propel objects with some php magic... nevermind.
        // If one wants similar solution, interface like IOwnableByUserAndGroup with
        // getUserId and getGroupId methods may be defined and used
        return is_subclass_of($object, "Acme\\AcmeBundle\\CommonBusinessObject");
    }       

    function vote(TokenInterface $token, $object, array $attributes)
    {   

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

        if (is_string($object)) {
            $scope = 'own';
            $entity = $object;
        } else {
            if ($object->getUserId() == $this->getUser()->getId()) {
                $scope = 'own';
            } else if ($object->getGroupId() == $this->getUser()->getGroupId()) {
                $scope = 'group';
            } else {
                $scope = 'others';
            }
            $entity = get_class($object);
        }

        $user = $token->getUser();
        $roles = $user->getRoles();
        $role = empty($roles) ? 'ROLE_USER' : $roles[0];

        $rights = $this->getRightsFor($role, $scope, $entity);
        if ($rights === null) return self::ACCESS_ABSTAIN;

        // some complicated logic for checking rights...
        foreach ($attributes as $attr) {
            $a = $attr;
            $field = '';
            if (preg_match("/^(EDIT|VIEW)((?:_[A-Z]+)+)$/", $attr, $m)) list(, $a, $field) = $m;
            if (!array_key_exists($a, $rights)) return self::ACCESS_DENIED;
            if ($rights[$a]) {
                if ($rights[$a] === true
                or  $field === '')
                    return self::ACCESS_GRANTED;
            }
            if (is_array($rights[$a])) {
                if ($field == '') return self::ACCESS_GRANTED;
                $rfield = ltrim(strtolower($field), '_');
                if (in_array($rfield, $rights[$a])) return self::ACCESS_GRANTED;
            }

            return self::ACCESS_DENIED;
        }
    }

    private function getRightsFor($role, $scope, $entity)
    {
        if (array_key_exists($entity, $this->rightsConfig)) {
            $rc = $this->rightsConfig[$entity];
        } else {
            $rc = $this->rightsConfig['global'];
        }
        $rc = $rc[$role][$scope];
        $ret = array();
        foreach($rc as $k => $v) {
            if (is_numeric($k)) $ret[$v] = true;
            else $ret[$k] = $v;
        }
        // hacky way to emulate cumulative rights like in ACL
        if (isset($ret['OWNER'])) $ret['MASTER'] = true;
        if (isset($ret['MASTER'])) $ret['OPERATOR'] = true;
        if (isset($ret['OPERATOR']))
            foreach(array('VIEW', 'EDIT', 'CREATE', 'DELETE', 'UNDELETE') as $r) $ret[$r] = true;
        return $ret;
    }

    private function getUser() {
        if (empty($this->user)) {
            // Not sure, how this shortcut works. This is a service (?) returning current authorized user.
            $this->user = $this->container->get('acme.user.shortcut');
        }
        return $this->user;
    }

}

И config ... на самом деле, он зависит от реализации и его структура совершенно произвольна.

grouped_concern_voter.config:
    global:
        ROLE_SUPERADMIN:
            own: [MASTER]
            group: [MASTER]
            others: [MASTER]
        ROLE_ADMIN:
            own: [MASTER]
            group: [MASTER]
            others: []
        ROLE_USER:
            own: [VIEW, EDIT, CREATE]
            group: [VIEW]
            others: []
    "Acme\\AcmeBundle\\User":
        # rights for ROLE_SUPERADMIN are derived from 'global'
        ROLE_ADMIN:
            own:
                VIEW: [login, email, real_name, properties, group_id]
                EDIT: [login, password, email, real_name, properties]
                CREATE: true
            group:
                VIEW: [login, email, real_name, properties]
                EDIT: [login, password, email, real_name, properties]
            # rights for ROLE_ADMIN/others are derived from 'global'
        ROLE_USER:
            own:
                VIEW: [login, password, email, real_name, properties]
                EDIT: [password, email, real_name, properties]
            group: []
            # rights for ROLE_USER/others are derived from 'global'
    "Acme\\AcmeBundle\\Cake":
        # most rights are derived from global here.
        ROLE_ADMIN:
            others: [VIEW]
        ROLE_USER:
            own: [VIEW]
            others: [VIEW]

И, наконец, пример использования. Где-то в контроллере:

$cake = Acme\AcmeBundle\CakeFactory->produce('strawberry', '1.3kg');
$securityContext = $this->get('security.context');
if ($securityContext->isGranted('EAT', $cake)) {
    die ("The cake is a lie");
}
person kirilloid    schedule 21.08.2012

при создании группы создайте роль ROLE_GROUP_ (идентификатор группы), продвиньте группу с этой ролью и предоставьте разрешения с помощью rolesecurityidentity

person Michał Kluczka    schedule 21.09.2012
comment
Я думал об этом, но в группе разные роли. Также мне следует обновить все роли, если пользователь меняет группу. - person kirilloid; 21.09.2012
comment
@kirilloid, вы всегда можете создать ROLE_GROUP_A_ (идентификатор группы), ROLE_GROUP_B_ (идентификатор группы) и так далее, и я действительно не понимаю, что вы имеете в виду под обновлением всех ролей, если пользователь меняет группу (?), когда пользователь меняет группу, которую он имеет автоматически ролей из этой новой группы и не имеют ролей предыдущей - person Michał Kluczka; 01.10.2012
comment
Изменение правил игры требует обновления кучи записей ACL. На днях решил, что есть новый правильный ВКУС, и любой может попробовать любой торт. В случае групп в качестве ролей я должен написать новый код и выполнить миграцию БД, обновляя текущий ACL. В случае моей конфигурации я должен обновить только одну строку в конфигурации: ROLE_USER: others: [TASTE] - person kirilloid; 01.10.2012