Как использовать 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?

Предполагая, что у вас есть набор объектов домена, которые вы хотите проверить, вы можете использовать метод findAcls() службы 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-системы и отчитаться здесь.

Невозможно провести маршрут по объектам, если у вас есть несколько тысяч объектов – он будет продолжать замедляться и потреблять больше памяти, заставляя вас использовать дозированные функции доктрины, делая ваш код более сложным (и неэффективным, потому что в конце концов вам нужно только ids, чтобы сделать запрос – не все 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 или group CreatedBy, по критериям из другой таблицы. Некоторые группы пользователей или учетные записи.

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