Symfony cumulative ACL isGranted

The default use of Access Control Lists in symfony can be a little awkward. That's because the $securityContext isGranted method performs a cumulative permissions check, while the typical implementations of ACL component isGranted perform a different kind of check. Here is what I mean, let's start with a simple security token.

$em = $this->getContainer()->get('doctrine')->getManager();
$aclManager = $this->getContainer()->get('myapp_user.acl_manager');

$repository = $em->getRepository('MyAppUserBundle:User');
$user = $repository->find(1);

$output->writeln(sprintf("user:%d %s", $user->getId(), $user->getUsername() ) );
$token = new UsernamePasswordToken($user, $user->getPassword(), "firewallname", $user->getRoles());
$securityContext = $this->getContainer()->get('security.token_storage');
$authorizationChecker = $this->getContainer()->get('security.authorization_checker'); 
$securityContext->setToken($token);

Then let's print off an ACL record for an example object 

$aclManager = $this->getContainer()->get('myapp_user.acl_manager');
$repository = $em->getRepository('MyAppCoreBundle:Comment');
$entity = $repository->find(1);
$acl = $aclManager->getAcl($entity);

foreach ( $acl->getObjectAces() as $ace ) {
    $output->writeln(sprintf("%s %s\n", $ace->getSecurityIdentity(), $ace->getMask()););
}

We get a fairly straightforward 

RoleSecurityIdentity(ROLE_GROUP_1) 1
RoleSecurityIdentity(ROLE_GROUP_OWNER_1) 128
UserSecurityIdentity(test@labstep.com, LabStep\UserBundle\Entity\User) 128

There is a single user, who has owner permissions over the comment object. There is also some group (GROUP_1), its users can view (1) and its owners can own (128) the comment. Let's confirm that...


$output->writeln((false === $authorizationChecker->isGranted("VIEW", $entity) ? "not granted": "granted"));
$output->writeln((false === $authorizationChecker->isGranted("OWNER", $entity) ? "not granted": "granted"));
granted
granted

Ok, awesome – the user has OWNER (128) permission – so both OWNER and VIEW are granted: duh, of course an owner can view what they own.

Let's use some standard isGranted implementation to check the roles, like the one from here

$securityIdentity = new RoleSecurityIdentity(sprintf("ROLE_GROUP_OWNER_%d", 1));
$output->writeln((false === $aclManager->isGranted("VIEW", $entity, $securityIdentity) ? "not granted": "granted" ));
$output->writeln((false === $aclManager->isGranted("OWNER", $entity, $securityIdentity) ? "not granted": "granted" ));
not granted
granted

The ROLE_GROUP_OWNER_1 is OWNER of this entity but is not granted to VIEW this entity? WTF? Clearly this check is not cumulative – being an owner doesn't equate being able to view the entity. This is very confusing but is nonetheless correct. So what does the authorization checker do under the hood, to provide a different behavior to the $aclManager? 

Well the following file
vendor//symfony/symfony/src/Symfony/Component/Security/Acl/Permission/BasicPermissionMap.php
Provides a mapping that basically means that an owner permission will be transformed into a mask that includes all the masks below OWNER in hierarchy. 

Here is an alternative example of isGranted that gives a more intuitive behavior to the commonly found example

use Symfony\Component\Security\Acl\Permission\BasicPermissionMap; 
 
public function isGranted($mask, $object, $securityIdentities)  {
    $objectIdentity = ObjectIdentity::fromDomainObject($object);
    if (!is_array($securityIdentities)) {
        $securityIdentities = array($securityIdentities);
    }
    // Find all the ACL records
    try {
        $acl = $this->provider->findAcl($objectIdentity, $securityIdentities);
    } catch (AclNotFoundException $e) {
        return false;
    }

    if (!is_int($mask)) {
        $permissionMap = new BasicPermissionMap();
        $masks = $permissionMap->getMasks($mask, null);
    }
    else {
        $masks = arary($mask);
    }

    // Perform the check
    try {
        return $acl->isGranted($masks, $securityIdentities, false);
    } catch (NoAceFoundException $e) {
        return false;
    }
}


Edits: the title was changed - it wasn't really descriptive of the post. Added a missing 'use' statement for the BasicPermissionMap

Comments

Popular posts from this blog

React.js – edit and delete comments in CommentBox

ROCS-like shape overlap in rdkit

Example slurm cluster on your laptop (multiple VMs via vagrant)