在Web应用程序中使用Symfony2中的ACL实现时,我们遇到了一个用例,其中建议的使用ACL的方法(检查单个域对象的用户权限)变得不可行。因此,我们想知道是否存在可用于解决问题的ACL API的某些部分。
用例位于控制器中,该控制器准备要在模板中显示的域对象列表,以便用户可以选择要编辑的对象。用户无权编辑数据库中的所有对象,因此必须相应地过滤列表。
这可以(根据其他解决方案)根据两种策略完成:
1)一个查询过滤器,它使用当前用户的ACL中的对象(或多个对象)附加有效对象id的给定查询。即:
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中是否有任何一种策略的实现(或实现预期结果的东西)?
答案 0 :(得分:20)
假设您有一组要检查的域对象,可以使用security.acl.provider
服务的findAcls()
方法在isGranted()
调用之前批量加载。
<强>条件:
数据库填充了测试实体,对我的数据库中的随机用户具有MaskBuilder::MASK_OWNER
的对象权限,对角色MASK_VIEW
具有IS_AUTHENTICATED_ANONYMOUSLY
的类权限; MASK_CREATE
的{{1}};以及ROLE_USER
的{{1}}和MASK_EDIT
。
测试代码:
MASK_DELETE
<强>结果:
通过对ROLE_ADMIN
的调用,分析器显示我的请求包含3个数据库查询(作为匿名用户)。
没有对$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
}
}
的调用,同一请求包含51个查询。
请注意,$aclProvider->findAcls($oids);
方法以30个批次(每批2个查询)加载,因此您的查询数量将随着更大的数据集而增加。该测试在工作日结束后约15分钟内完成;如果有机会,我会仔细检查相关方法,看看ACL系统是否还有其他有用用途,并在此报告。
答案 1 :(得分:9)
如果你有几千个实体,那么对实体进行实例化是不可行的 - 它会不断变慢并消耗更多内存,迫使你使用学说批处理功能,从而使你的代码变得更加复杂(而且因为毕竟你的代码是无用的只需要id来进行查询 - 而不是内存中的整个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)
);
}
然后从获取实体ID的实际公共方法中调用此方法:
/**
* 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级别)
答案 2 :(得分:0)
将Symfony ACL耦合回应用程序并将其用作排序,并不是一个好方法。您正在混合并将2层或3层应用程序耦合在一起。 ACL功能是回答“是/否”以质疑“我允许这样做吗?”如果您需要某种拥有/可编辑的文章,您可以使用CreatedBy之类的列或按另一个表中的条件组CreatedBy。一些用户组或帐户。
答案 3 :(得分:-3)
使用连接,如果您正在使用Doctrine,请让它为您生成连接,因为它们几乎总是更快。因此,您应该设计ACL模式,以便执行这些快速过滤器。