Как использовать 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?
4 ответа
Предполагая, что у вас есть коллекция объектов домена, которые вы хотите проверить, вы можете использовать security.acl.provider
Сервисы 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 (с 2 запросами на пакет), поэтому количество запросов увеличится с большими наборами данных. Этот тест был сделан примерно через 15 минут в конце рабочего дня; когда у меня будет возможность, я пройду и рассмотрю соответствующие методы более тщательно, чтобы увидеть, есть ли другие полезные применения системы ACL, и сообщу здесь.
Перемещение по сущностям неосуществимо, если у вас есть пара тысячных сущностей - оно будет продолжать работать медленнее и потреблять больше памяти, заставляя вас использовать возможности пакетной обработки доктрин, что делает ваш код более сложным (и неэффективным, потому что в конце концов вам нужен только идентификаторы, чтобы сделать запрос - не все 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)
Соединение Symfony ACL с приложением и использование его в качестве сортировки не является хорошим подходом. Вы смешиваете и соединяете 2 или 3 слоя приложения вместе. Функциональность ACL - отвечать "ДА / НЕТ" на вопрос "Разрешено ли мне это делать?" Если вам нужны какие-то собственные / редактируемые статьи, вы можете использовать какой-то столбец, например CreatedBy или группу CreatedBy, по критериям из другой таблицы. Некоторые группы пользователей или учетные записи.
Используйте объединения, и, если вы используете Doctrine, используйте его для создания объединений, поскольку они почти всегда быстрее. Поэтому вы должны разработать свою схему ACL, чтобы эти быстрые фильтры были выполнимы.