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);
$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
Edits: the title was changed - it wasn't really descriptive of the post. Added a missing 'use' statement for the BasicPermissionMap
Comments
Post a Comment