如何根据特定用户的权限(例如编辑)使用ACL过滤域对象列表?

Ale*_*ski 32 php permissions acl domain-object symfony

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

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

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

1)一个查询过滤器,它使用当前用户的ACL为对象(或多个对象)附加有效对象id的给定查询.即:

WHERE <other conditions> AND u.id IN(<list of legal object ids here>)
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

第一种策略是可取的,因为数据库正在进行所有过滤工作,并且都需要两次数据库查询.一个用于ACL,一个用于实际查询,但这可能是不可避免的.

在Symfony2中是否有任何一种策略(或实现预期结果)的实现?

Pro*_*tic 20

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

条件:

数据库填充了测试实体,具有MaskBuilder::MASK_OWNER来自我的数据库的随机用户的对象权限,以及MASK_VIEW角色的类权限IS_AUTHENTICATED_ANONYMOUSLY; MASK_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
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

通过调用$aclProvider->findAcls($oids);,分析器显示我的请求包含3个数据库查询(作为匿名用户).

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

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

  • 但我不知道你想用这个来处理更大的数据集.想象一下,你有10000条记录,即使有了分页,我也必须知道用户拥有多少条记录.有没有办法不遍历表中的所有实体? (8认同)
  • 我完全同意@stoefln,这样的解决方案对于更大的数据集是不切实际的.[迭戈的答案](http://stackoverflow.com/a/7452467/539560)看起来更好但我不确定... (2认同)

小智 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)
    );

} 
Run Code Online (Sandbox Code Playgroud)

然后从获取实体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;
}
Run Code Online (Sandbox Code Playgroud)

这样我们就可以使用代码为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
Run Code Online (Sandbox Code Playgroud)

缺点:必须改进这种方法,以考虑ACL继承和策略的复杂性,但对于简单的用例,它可以正常工作.还必须实现缓存以避免重复的双重查询(一个具有类级别,另一个具有objetc级别)