【问题标题】:How do one use ACL to filter a list of domain-objects according to a certain user's permissions (e.g. EDIT)?如何使用 ACL 根据某个用户的权限(例如 EDIT)过滤域对象列表?
【发布时间】:2011-10-01 01:30:30
【问题描述】:

当在 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 中是否有实施其中一种策略(或实现预期结果的方法)?

【问题讨论】:

    标签: php permissions acl symfony domain-object


    【解决方案1】:

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

    条件:

    数据库中填充了测试实体,我的数据库中随机用户的对象权限为MaskBuilder::MASK_OWNER,角色IS_AUTHENTICATED_ANONYMOUSLY 的类权限为MASK_VIEWMASK_CREATEROLE_USER;和MASK_EDITMASK_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-&gt;findAcls($oids);,分析器显示我的请求包含 3 个数据库查询(作为匿名用户)。

    没有调用findAcls(),同一个请求包含51个查询。

    请注意,findAcls() 方法以 30 个批次加载(每批次 2 个查询),因此您的查询数量会随着数据集的增大而增加。该测试在工作日结束时大约 15 分钟内完成;如果有机会,我会更彻底地检查相关方法,看看 ACL 系统是否还有其他有用的用途,并在此报告。

    【讨论】:

    • 但我不明白你想如何处理更大的数据集。想象一下,您有 10000 条记录,即使有分页,我也必须知道用户拥有多少条记录。有没有办法不遍历表中的所有实体?
    • 我完全同意@stoefln,这种解决方案对于更大的数据集是不切实际的。 Diego's answer 看起来更好,但我不确定...
    • +1 在无望地盯着文档看了一会儿之后,我终于有了一点(稍微无关)但非常有帮助的 ACL 顿悟,阅读了这个答案。谢谢你:)
    【解决方案2】:

    如果您有几千个实体,遍历实体是不可行的 - 它会变得越来越慢并消耗更多内存,迫使您使用学说批处理功能,从而使您的代码更加复杂(并且无用,因为毕竟您只需要 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 继承和策略的复杂性,必须改进这些方法,但对于简单的用例,它可以正常工作。还必须实现缓存以避免重复的双重查询(一个是类级别,另一个是对象级别)

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

        【讨论】:

        • 我们在 Symfony2 中使用 ACL 实现,因此我们对 ACL 模式及其复杂性没有影响。鉴于 Symonfy'2 ACL 模式的结构,用这种连接附加每个查询是不可行的(主要是因为身份间接、ACE 层次结构和 ACE 存在)。我将编辑问题以更清楚地说明我们正在使用 Symfony2 的 ACL 实现。
        猜你喜欢
        • 1970-01-01
        • 2013-11-22
        • 2015-06-12
        • 2011-09-30
        • 1970-01-01
        • 1970-01-01
        • 2012-11-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多