33

当在 Web 应用程序中使用 Symfony2 中的 ACL 实现时,我们遇到了一个使用 ACL 的建议方法(检查单个域对象上的用户权限)变得不可行的用例。因此,我们想知道是否存在可以用来解决问题的 ACL API 的某些部分。

用例位于控制器中,该控制器准备要在模板中显示的域对象列表,以便用户可以选择她想要编辑的对象。用户无权编辑数据库中的所有对象,因此必须对列表进行相应过滤。

这可以(在其他解决方案中)根据两种策略来完成:

1) 一个查询过滤器,它使用来自当前用户的对象(或多个对象)的 ACL 中的有效对象 ID 附加给定查询。IE:

WHERE <other conditions> AND u.id IN(<list of legal object ids here>)

2) 后查询过滤器,在从数据库中检索到完整列表后,删除用户没有正确权限的对象。IE:

$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

4 回答 4

20

假设您有一组要检查的域对象,您可以使用security.acl.provider服务的findAcls()方法在isGranted()调用之前批量加载。

条件:

数据库中填充了测试实体,对象权限MaskBuilder::MASK_OWNER为我数据库中的随机用户,类权限MASK_VIEW为角色IS_AUTHENTICATED_ANONYMOUSLYMASK_CREATE对于 ROLE_USER; 并且对于。MASK_EDIT_MASK_DELETEROLE_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 系统有用的用途,并在此报告。

于 2011-09-06T23:36:36.143 回答
9

如果你有几千个实体,遍历实体是不可行的——它会越来越慢并消耗更多的内存,迫使你使用学说批处理功能,从而使你的代码更复杂(并且无用,因为毕竟你只需要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)
    );

} 

然后从获取实体 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 级别)

于 2011-09-17T04:39:42.667 回答
0

将 Symfony ACL 耦合回应用程序并将其用作排序,这不是一个好方法。您将 2 或 3 层应用程序混合并耦合在一起。ACL 功能是回答“是/否”来质疑“我可以这样做吗?” 如果您需要某种拥有/可编辑的文章,您可以使用 CreatedBy 之类的列或按另一个表中的条件对 CreatedBy 进行分组。一些用户组或帐户。

于 2015-09-11T23:38:02.470 回答
-3

使用连接,如果你使用 Doctrine,让它为你生成连接,因为它们几乎总是更快。因此,您应该设计您的 ACL 模式,以使执行这些快速过滤器是可行的。

于 2011-07-08T19:13:11.940 回答