Подтвердить что ты не робот

Как использовать ACL для фильтрации списка объектов домена в соответствии с определенными правами пользователя (например, EDIT)?

При использовании реализации 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?

4b9b3361

Ответ 1

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

Условия:

База данных была заполнена тестовыми объектами, с объектными разрешениями MaskBuilder::MASK_OWNER для случайного пользователя из моей базы данных и разрешениями класса MASK_VIEW для роли IS_AUTHENTICATED_ANONYMOUSLY; MASK_CREATE для ROLE_USER; и MASK_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 (с двумя запросами на пакет), поэтому количество запросов будет увеличиваться с большими наборами данных. Это испытание было выполнено примерно через 15 минут в конце рабочего дня; когда у меня есть шанс, я рассмотрю и рассмотрю соответствующие методы более подробно, чтобы узнать, есть ли какие-либо другие полезные функции ACL-системы и отчитаться здесь.

Ответ 2

Невозможно переназначить сущности, если у вас есть несколько тысячных сущностей - он будет продолжать замедляться и потреблять больше памяти, заставляя вас использовать дозированные функции доктрины, тем самым делая ваш код более сложным (и неэффективным, потому что после вас нужны только идентификаторы для запроса - не все 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 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)

Ответ 3

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

Ответ 4

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