Как использовать 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?
Ответы
Ответ 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, чтобы выполнять эти быстрые фильтры.