Как использовать ACL для фильтрации списка доменных объектов в соответствии с разрешениями определенного пользователя (например, EDIT)? - PullRequest
32 голосов
/ 08 июля 2011

При использовании реализации ACL в Symfony2 в веб-приложении мы сталкивались со случаем, когда предлагаемый способ использования ACL (проверка прав доступа пользователей к одному объекту домена) становится неосуществимым. Таким образом, мы задаемся вопросом, существует ли какая-либо часть API ACL, которую мы можем использовать для решения нашей проблемы.

Вариант использования находится в контроллере, который подготавливает список объектов домена для представления в шаблоне, чтобы пользователь мог выбрать, какой из своих объектов он хочет редактировать. У пользователя нет прав на редактирование всех объектов в базе данных, поэтому список необходимо соответствующим образом отфильтровать.

Это можно сделать (среди прочих решений) в соответствии с двумя стратегиями:

1) Фильтр запросов, который добавляет данный запрос с действительными идентификаторами объектов из ACL текущего пользователя для объекта (или объектов). То есть:

WHERE <other conditions> AND u.id IN(<list of legal object ids here>)

2) Фильтр пост-запроса, который удаляет объекты, для которых у пользователя нет правильных разрешений после получения полного списка из базы данных. То есть:

$objs   = <query for objects>
$objIds = <getting all the permitted obj ids from the ACL>
for ($obj in $objs) {
    if (in_array($obj.id, $objIds) { $result[] = $obj; } 
}
return $result;

Первая стратегия предпочтительнее, поскольку база данных выполняет всю работу по фильтрации, и обе требуют двух запросов к базе данных. Один для ACL и один для фактического запроса, но это, вероятно, неизбежно.

Есть ли какая-либо реализация одной из этих стратегий (или что-то, что дает желаемые результаты) в Symfony2?

Ответы [ 4 ]

20 голосов
/ 07 сентября 2011

Предполагая, что у вас есть набор доменных объектов, которые вы хотите проверить, вы можете использовать метод findAcls() службы security.acl.provider для пакетной загрузки до вызовов isGranted().

Условия:

База данных была заполнена тестовыми объектами с разрешениями объекта MaskBuilder::MASK_OWNER для случайного пользователя из моей базы данных и разрешениями класса MASK_VIEW для роли IS_AUTHENTICATED_ANONYMOUSLY;MASK_CREATE для ROLE_USERMASK_EDIT и MASK_DELETE для ROLE_ADMIN.

Код теста:

$repo = $this->getDoctrine()->getRepository('Foo\Bundle\Entity\Bar');
$securityContext = $this->get('security.context');
$aclProvider = $this->get('security.acl.provider');

$barCollection = $repo->findAll();

$oids = array();
foreach ($barCollection as $bar) {
    $oid = ObjectIdentity::fromDomainObject($bar);
    $oids[] = $oid;
}

$aclProvider->findAcls($oids); // preload Acls from database

foreach ($barCollection as $bar) {
    if ($securityContext->isGranted('EDIT', $bar)) {
        // permitted
    } else {
        // denied
    }
}

РЕЗУЛЬТАТЫ:

При вызове $aclProvider->findAcls($oids);, профилировщик показывает, что мой запрос содержал 3 запроса к базе данных (как анонимный пользователь).

Без вызов findAcls()тот же запрос содержал 51 запрос.

Обратите внимание, что метод findAcls() загружается партиями по 30 (с 2 запросами на пакет), поэтому число запросов будет увеличиваться с большими наборами данных.Этот тест был сделан примерно через 15 минут в конце рабочего дня;когда у меня будет возможность, я пройду и рассмотрю соответствующие методы более тщательно, чтобы увидеть, есть ли другие полезные варианты использования системы ACL, и доложу здесь.

9 голосов
/ 17 сентября 2011

Перемещение по сущностям неосуществимо, если у вас есть пара тысячных сущностей - оно будет продолжать работать медленнее и потреблять больше памяти, заставляя вас использовать возможности пакетной обработки доктрин, тем самым делая ваш код более сложным (и неэффективным, потому что в конце концовдля выполнения запроса нужны только идентификаторы, а не целые acl / сущности в памяти)

Чтобы решить эту проблему, мы заменили службу acl.provider своей собственной, а в эту службу добавили методпрямой запрос к базе данных:

private function _getEntitiesIdsMatchingRoleMaskSql($className, array $roles, $requiredMask)
{
    $rolesSql = array();
    foreach($roles as $role) {
        $rolesSql[] = 's.identifier = ' . $this->connection->quote($role);
    }
    $rolesSql =  '(' . implode(' OR ', $rolesSql) . ')';

    $sql = <<<SELECTCLAUSE
        SELECT 
            oid.object_identifier
        FROM 
            {$this->options['entry_table_name']} e
        JOIN 
            {$this->options['oid_table_name']} oid ON (
            oid.class_id = e.class_id
        )
        JOIN {$this->options['sid_table_name']} s ON (
            s.id = e.security_identity_id
        )     
        JOIN {$this->options['class_table_nambe']} class ON (
            class.id = e.class_id
        )
        WHERE 
            {$this->connection->getDatabasePlatform()->getIsNotNullExpression('e.object_identity_id')} AND
            (e.mask & %d) AND
            $rolesSql AND
            class.class_type = %s
       GROUP BY
            oid.object_identifier    
SELECTCLAUSE;

    return sprintf(
        $sql,
        $requiredMask,
        $this->connection->quote($role),
        $this->connection->quote($className)
    );

} 

Затем вызывается этот метод из фактического открытого метода, который получает идентификаторы сущностей:

/**
 * Get the entities Ids for the className that match the given role & mask
 * 
 * @param string $className
 * @param string $roles
 * @param integer $mask 
 * @param bool $asString - Return a comma-delimited string with the ids instead of an array
 * 
 * @return bool|array|string - True if its allowed to all entities, false if its not
 *          allowed, array or string depending on $asString parameter.
 */
public function getAllowedEntitiesIds($className, array $roles, $mask, $asString = true)
{

    // Check for class-level global permission (its a very similar query to the one
    // posted above
    // If there is a class-level grant permission, then do not query object-level
    if ($this->_maskMatchesRoleForClass($className, $roles, $requiredMask)) {
        return true;
    }         

    // Query the database for ACE's matching the mask for the given roles
    $sql = $this->_getEntitiesIdsMatchingRoleMaskSql($className, $roles, $mask);
    $ids = $this->connection->executeQuery($sql)->fetchAll(\PDO::FETCH_COLUMN);

    // No ACEs found
    if (!count($ids)) {
        return false;
    }

    if ($asString) {
        return implode(',', $ids);
    }

    return $ids;
}

Таким образом, теперь мы можем использовать код для добавленияфильтры к DQL-запросам:

// Some action in a controller or form handler...

// This service is our own aclProvider version with the methods mentioned above
$aclProvider = $this->get('security.acl.provider');

$ids = $aclProvider->getAllowedEntitiesIds('SomeEntityClass', array('role1'), MaskBuilder::VIEW, true);

if (is_string($ids)) {
   $queryBuilder->andWhere("entity.id IN ($ids)");
}
// No ACL found: deny all
elseif ($ids===false) {
   $queryBuilder->andWhere("entity.id = 0")
}
elseif ($ids===true) {
   // Global-class permission: allow all
}

// Run query...etc

Недостатки: Эти методы необходимо усовершенствовать, чтобы учесть сложности наследования и стратегий ACL, но для простых случаев использования он работает нормально.Также должен быть реализован кэш, чтобы избежать повторяющегося двойного запроса (один с уровнем класса, другой с уровнем objetc)

0 голосов
/ 12 сентября 2015

Соединение Symfony ACL с приложением и использование его в качестве сортировки не является хорошим подходом. Вы смешиваете и соединяете 2 или 3 слоя приложения вместе. Функциональность ACL - отвечать «ДА / НЕТ» на вопрос «Разрешено ли мне это делать?» Если вам нужны какие-то собственные / редактируемые статьи, вы можете использовать какой-то столбец, например, CreatedBy или группу CreatedBy по критериям из другой таблицы. Некоторые группы пользователей или аккаунты.

0 голосов
/ 08 июля 2011

Используйте объединения, и, если вы используете Doctrine, используйте его для создания объединений, поскольку они почти всегда быстрее.Поэтому вы должны разработать свою схему ACL, чтобы эти быстрые фильтры были выполнимы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...