Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do one use ACL to filter a list of domain-objects according to a certain user's permissions (e.g. EDIT)?

When using the ACL implementation in Symfony2 in a web application, we have come across a use case where the suggested way of using the ACLs (checking a users permissions on a single domain object) becomes unfeasible. Thus, we wonder if there exists some part of the ACL API we can use to solve our problem.

The use case is in a controller that prepares a list of domain objects to be presented in a template, so that the user can choose which of her objects she wants to edit. The user does not have permission to edit all of the objects in the database, so the list must be filtered accordingly.

This could (among other solutions) be done according to two strategies:

1) A query filter that appends a given query with the valid object ids from the present user's ACL for the object(or objects). I.e:

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

2) A post-query filter that removes the objects the user does not have the correct permissions for after the complete list has been retrieved from the database. I.e:

$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; 

The first strategy is preferable as the database is doing all the filtering work, and both require two database queries. One for the ACLs and one for the actual query, but that is probably unavoidable.

Is there any implementation of one of these strategies (or something achieving the desired results) in Symfony2?

like image 807
Aleksander Krzywinski Avatar asked Jul 08 '11 07:07

Aleksander Krzywinski


People also ask

What is an ACL database?

An access control list (ACL) is a list of rules that specifies which users or systems are granted or denied access to a particular object or system resource. Access control lists are also installed in routers or switches, where they act as filters, managing which traffic can access the network.

What is ACL policy?

An access control list policy, or ACL policy, is the set of rules (permissions) that specifies the conditions necessary to perform certain operations on that resource. ACL policy definitions are important components of the security policy established for the secure domain.

What are the options ACLs should meet to grant the access?

With ACLs you can grant either read-only access, or read-write access on a directory or file to specific users. At this point, bob could access and modify the “shared-data. txt” file. Now the user bob can copy or save files in the “shared” directory.

What is ACL in active directory?

An access-control list (ACL) is the ordered collection of access control entries defined for an object. A security descriptor supports properties and methods that create and manage ACLs. For more information about security models, see Security or the Windows 2000 Server Resource Kit.


2 Answers

Assuming that you have a collection of domain objects that you want to check, you can use the security.acl.provider service's findAcls() method to batch load in advance of the isGranted() calls.

Conditions:

Database was populated with test entities, with object permissions of MaskBuilder::MASK_OWNER for a random user from my database, and class permissions of MASK_VIEW for role IS_AUTHENTICATED_ANONYMOUSLY; MASK_CREATE for ROLE_USER; and MASK_EDIT and MASK_DELETE for ROLE_ADMIN.

Test Code:

$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     } } 

RESULTS:

With the call to $aclProvider->findAcls($oids);, the profiler shows that my request contained 3 database queries (as anonymous user).

Without the call to findAcls(), the same request contained 51 queries.

Note that the findAcls() method loads in batches of 30 (with 2 queries per batch), so your number of queries will go up with larger datasets. This test was done in about 15 minutes at the end of the work day; when I have a chance, I'll go through and review the relevant methods more thoroughly to see if there are any other helpful uses of the ACL system and report back here.

like image 135
Derek Stobbe Avatar answered Oct 12 '22 02:10

Derek Stobbe


Itinerating over the entities is not feasible if you have a couple of thousandth entities - it will keep getting slower and consuming more memory, forcing you to use doctrine batching capabilities, thus making your code more complex (and innefective because after all you need only the ids to make a query - not the whole acl/entities in memory)

What we did to solve this problem is to replace acl.provider service with our own and in that service add a method to make a direct query to the database:

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)     );  }  

Then calling this method from the actual public method that gets the entities ids:

/**  * 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; } 

This way now we can use the code to add filters to DQL queries:

// 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 

Drawbacks: This methods have to be improved to take into account the complexities of ACL inheritance and strategies, but for simple use cases it works fine. Also a cache has to be implemented to avoid the repetitive double query (one with class-level, another with objetc-level)

like image 40
Diego Avatar answered Oct 12 '22 03:10

Diego