diff --git a/.gitignore b/.gitignore index 0bfe817..195a069 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ vendor/* composer.lock + +Tests/App/cache +Tests/App/logs \ No newline at end of file diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..2c2c055 --- /dev/null +++ b/.php_cs @@ -0,0 +1,15 @@ +level(Symfony\CS\FixerInterface::PSR2_LEVEL) + ->fixers(array( + 'ordered_use', + 'multiline_spaces_before_semicolon', + 'concat_with_spaces' + )) + ->finder( + Symfony\CS\Finder\DefaultFinder::create() + ->exclude('vendor') + ->in(__DIR__) + ) +; \ No newline at end of file diff --git a/Domain/AbstractAclManager.php b/Domain/AbstractAclManager.php index cd728f7..de953d5 100644 --- a/Domain/AbstractAclManager.php +++ b/Domain/AbstractAclManager.php @@ -6,6 +6,7 @@ use Problematic\AclManagerBundle\Model\AclManagerInterface; use Problematic\AclManagerBundle\Model\PermissionContextInterface; use Problematic\AclManagerBundle\RetrievalStrategy\AclObjectIdentityRetrievalStrategyInterface; +use Symfony\Component\Security\Acl\Domain\Entry; use Symfony\Component\Security\Acl\Domain\ObjectIdentity; use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; @@ -14,6 +15,7 @@ use Symfony\Component\Security\Acl\Model\MutableAclProviderInterface; use Symfony\Component\Security\Acl\Model\ObjectIdentityInterface; use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface; +use Symfony\Component\Security\Acl\Permission\MaskBuilder; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Role\RoleInterface; use Symfony\Component\Security\Core\SecurityContextInterface; @@ -67,7 +69,7 @@ public function __construct( /** * @return MutableAclProviderInterface */ - protected function getAclProvider() + public function getAclProvider() { return $this->aclProvider; } @@ -98,6 +100,7 @@ protected function getObjectIdentityRetrievalStrategy() protected function doLoadAcl(ObjectIdentityInterface $objectIdentity) { $acl = null; + try { $acl = $this->getAclProvider()->createAcl($objectIdentity); } catch (AclAlreadyExistsException $ex) { @@ -123,21 +126,21 @@ protected function doRemoveAcl($token) * Returns an instance of PermissionContext. If !$securityIdentity instanceof SecurityIdentityInterface, a new security identity will be created using it * * @param string $type - * @param string $field + * @param string|string[] $fields * @param $securityIdentity * @param integer $mask * @param boolean $granting * @return PermissionContext */ - protected function doCreatePermissionContext($type, $field, $securityIdentity, $mask, $granting = true) + protected function doCreatePermissionContext($type, $fields, $securityIdentity = null, $mask = null, $granting = true) { - if (!$securityIdentity instanceof SecurityIdentityInterface) { - $securityIdentity = $this->doCreateSecurityIdentity($securityIdentity); + if (!is_array($fields)) { + $fields = (array) $fields; } $permissionContext = new PermissionContext(); $permissionContext->setPermissionType($type); - $permissionContext->setField($field); + $permissionContext->setFields($fields); $permissionContext->setSecurityIdentity($securityIdentity); $permissionContext->setMask($mask); $permissionContext->setGranting($granting); @@ -153,8 +156,16 @@ protected function doCreatePermissionContext($type, $field, $securityIdentity, $ * * @return SecurityIdentityInterface */ - protected function doCreateSecurityIdentity($identity) + protected function doCreateSecurityIdentity($identity = null) { + if (null === $identity) { + $identity = $this->getUser(); + } + + if ($identity instanceof SecurityIdentityInterface) { + return $identity; + } + if (!$identity instanceof UserInterface && !$identity instanceof TokenInterface && !$identity instanceof RoleInterface && !is_string($identity)) { throw new \InvalidArgumentException(sprintf('$identity must implement one of: UserInterface, TokenInterface, RoleInterface (%s given)', get_class($identity))); } @@ -177,37 +188,42 @@ protected function doCreateSecurityIdentity($identity) /** * Loads an ACE collection from the ACL and updates the permissions (creating if no appropriate ACE exists) + * @param MutableAclInterface $acl + * @param PermissionContextInterface $context + * @param bool $replaceExisting * - * @param MutableAclInterface $acl - * @param PermissionContextInterface $context - * @return void + * @throws \Doctrine\DBAL\ConnectionException + * @throws \Exception */ protected function doApplyPermission(MutableAclInterface $acl, PermissionContextInterface $context, $replaceExisting = false) { $type = $context->getPermissionType(); - $field = $context->getField(); + $this->connection->beginTransaction(); - if (is_null($field)) { - $aceCollection = $this->getAceCollection($acl, $type); - } else { - $aceCollection = $this->getFieldAceCollection($acl, $type, $field); - } + try { + $fields = $context->getFields(); + if (empty($fields)) { + $aceCollection = $this->getAceCollection($acl, $type); + $size = count($aceCollection) - 1; + reset($aceCollection); - $size = count($aceCollection) - 1; - reset($aceCollection); + $this->doUpdatePermission($size, $replaceExisting, $aceCollection, $context, $acl, null, $type); + } else { + foreach ($context->getFields() as $field) { + $aceCollection = $this->getFieldAceCollection($acl, $type, $field); - //If transaction already - if($this->connection->isTransactionActive()){ - $this->doUpdatePermission($size, $replaceExisting, $aceCollection, $context, $acl, $field, $type); - }else{ - try{ - $this->connection->beginTransaction(); - $this->doUpdatePermission($size, $replaceExisting, $aceCollection, $context, $acl, $field, $type); - $this->connection->commit(); - } catch(\Exception $e){ - $this->connection->rollBack(); - throw $e; + $size = count($aceCollection) - 1; + reset($aceCollection); + + $this->doUpdatePermission($size, $replaceExisting, $aceCollection, $context, $acl, $field, $type); + } } + + $this->connection->commit(); + } catch (\Exception $e) { + $this->connection->rollBack(); + + throw $e; } } @@ -227,7 +243,7 @@ protected function doUpdatePermission($size, $replaceExisting, $aceCollection, P // Replace all existing permissions with the new one if ($context->hasDifferentPermission($aceCollection[$i])) { // The ACE was found but with a different permission. Update it. - if (is_null($field)) { + if (null === $field) { $acl->{"update{$type}Ace"}($i, $context->getMask()); } else { $acl->{"update{$type}FieldAce"}($i, $field, $context->getMask()); @@ -249,10 +265,14 @@ protected function doUpdatePermission($size, $replaceExisting, $aceCollection, P } } + if (null === $securityIdentity = $context->getSecurityIdentity()) { + $securityIdentity = $this->doCreateSecurityIdentity(); + } + //If we come this far means we have to insert ace - if (is_null($field)) { + if (null === $field) { $acl->{"insert{$type}Ace"}( - $context->getSecurityIdentity(), + $securityIdentity, $context->getMask(), 0, $context->isGranting() @@ -260,7 +280,7 @@ protected function doUpdatePermission($size, $replaceExisting, $aceCollection, P } else { $acl->{"insert{$type}FieldAce"}( $field, - $context->getSecurityIdentity(), + $securityIdentity, $context->getMask(), 0, $context->isGranting() @@ -271,70 +291,175 @@ protected function doUpdatePermission($size, $replaceExisting, $aceCollection, P /** * @param MutableAclInterface $acl * @param PermissionContextInterface $context + * + * @throws \Doctrine\DBAL\ConnectionException + * @throws \Exception */ protected function doRevokePermission(MutableAclInterface $acl, PermissionContextInterface $context) { $type = $context->getPermissionType(); - $field = $context->getField(); + $fields = $context->getFields(); + $isTransactionActive = $this->connection->isTransactionActive(); - if (is_null($field)) { - $aceCollection = $this->getAceCollection($acl, $type); - } else { - $aceCollection = $this->getFieldAceCollection($acl, $type, $field); + if (!$isTransactionActive) { + $this->connection->beginTransaction(); } - $found = false; - $size = count($aceCollection) - 1; - reset($aceCollection); + try { + if (null === $fields || empty($fields)) { + $aceCollection = $this->getAceCollection($acl, $type); + + $found = false; + $size = count($aceCollection) - 1; + reset($aceCollection); + + for ($i = $size; $i >= 0; $i--) { + /** @var Entry $ace */ + $ace = $aceCollection[$i]; + + if (null === $context->getSecurityIdentity() || $ace->getSecurityIdentity() === $context->getSecurityIdentity()) { + if ($context->equals($ace)) { + $acl->{"delete{$type}Ace"}($i); + $found = true; + } + } + } - for ($i = $size; $i >= 0; $i--) { - //@todo: probably not working if multiple ACEs or different bit mask - // but that include these permissions. - if ($context->equals($aceCollection[$i])) { - if (is_null($field)) { - $acl->{"delete{$type}Ace"}($i); - } else { - $acl->{"delete{$type}FieldAce"}($i, $field); + if (false === $found) { + // create a non-granting ACE for this permission + + if (null === $securityIdentity = $context->getSecurityIdentity()) { + $securityIdentity = $this->doCreateSecurityIdentity(); + } + + $newContext = $this->doCreatePermissionContext( + $context->getPermissionType(), + null, + $securityIdentity, + $context->getMask(), + false + ); + + $this->doApplyPermission($acl, $newContext); + } + } else { + $aceCollection = array(); + + foreach ($fields as $field) { + foreach ($this->getFieldAceCollection($acl, $type, $field) as $ace) { + $aceCollection[] = $ace; + } + } + + $found = false; + $size = count($aceCollection) - 1; + reset($aceCollection); + + foreach ($fields as $field) { + for ($i = $size; $i >= 0; $i--) { + /** @var Entry $ace */ + $ace = $aceCollection[$i]; + + if ($context->equals($ace)) { + $acl->{"delete{$type}FieldAce"}($i, $field); + $found = true; + } + } + + if (false === $found) { + $newContext = $this->doCreatePermissionContext( + $context->getPermissionType(), + $field, + $context->getSecurityIdentity(), + $context->getMask(), + false + ); + + $this->doApplyPermission($acl, $newContext); + } } - $found = true; } - } - if (false === $found) { - // create a non-granting ACE for this permission - $newContext = $this->doCreatePermissionContext($context->getPermissionType(), $field, $context->getSecurityIdentity(), $context->getMask(), false); - $this->doApplyPermission($acl, $newContext); + if (!$isTransactionActive) { + $this->connection->commit(); + } + } catch (\Exception $e) { + if (!$isTransactionActive) { + $this->connection->rollBack(); + } + + throw $e; } } /** - * @param MutableAclInterface $acl - * @param SecurityIdentityInterface $securityIdentity - * @param string $type - * @param string|null $field + * @param MutableAclInterface $acl + * @param PermissionContextInterface $context + * + * @throws \Doctrine\DBAL\ConnectionException + * @throws \Exception */ - protected function doRevokeAllPermissions(MutableAclInterface $acl, SecurityIdentityInterface $securityIdentity, $type = 'object', $field = null) + protected function doRevokeAllPermissions(MutableAclInterface $acl, PermissionContextInterface $context) { - if (is_null($field)) { - $aceCollection = $this->getAceCollection($acl, $type); - } else { - $aceCollection = $this->getFieldAceCollection($acl, $type, $field); + $isActiveTransaction = $this->connection->isTransactionActive(); + $fields = $context->getFields(); + + if (!$isActiveTransaction) { + $this->connection->beginTransaction(); } - $size = count($aceCollection) - 1; - reset($aceCollection); + if (null === $fields || empty($fields)) { + $aceCollection = $this->getAceCollection($acl, $context->getPermissionType()); + + $size = count($aceCollection) - 1; + reset($aceCollection); + + try { + for ($i = $size; $i >= 0; $i--) { + /** @var Entry $ace */ + $ace = $aceCollection[$i]; + + if (null === $context->getSecurityIdentity() || $ace->getSecurityIdentity() === $context->getSecurityIdentity()) { + $acl->{"delete{$context->getPermissionType()}Ace"}($i); + } + } - if (is_null($field)) { - for ($i = $size; $i >= 0; $i--) { - if ($aceCollection[$i]->getSecurityIdentity() == $securityIdentity) { - $acl->{"delete{$type}Ace"}($i); + if (!$isActiveTransaction) { + $this->connection->commit(); + } + } catch (\Exception $e) { + if (!$isActiveTransaction) { + $this->connection->rollBack(); } + throw $e; } } else { - for ($i = $size; $i >= 0; $i--) { - if ($aceCollection[$i]->getSecurityIdentity() == $securityIdentity) { - $acl->{"delete{$type}FieldAce"}($i, $field); + try { + foreach ($fields as $field) { + $aceCollection = $this->getFieldAceCollection($acl, $context->getPermissionType(), $field); + + $size = count($aceCollection) - 1; + reset($aceCollection); + + for ($i = $size; $i >= 0; $i--) { + /** @var Entry $ace */ + $ace = $aceCollection[$i]; + + if (null === $context->getSecurityIdentity() || $ace->getSecurityIdentity() === $context->getSecurityIdentity()) { + $acl->{"delete{$context->getPermissionType()}FieldAce"}($i, $field); + } + } + } + + if (!$isActiveTransaction) { + $this->connection->commit(); + } + } catch (\Exception $e) { + if (!$isActiveTransaction) { + $this->connection->rollBack(); } + + throw $e; } } } @@ -365,4 +490,28 @@ protected function getFieldAceCollection(MutableAclInterface $acl, $type = 'obje return $aceCollection; } + + /** + * @param string|string[]|int $attributes + * + * @return int + */ + protected function buildMask($attributes) + { + if (is_int($attributes)) { //it's already a mask + return $attributes; + } + + if (!is_array($attributes)) { + $attributes = (array) $attributes; + } + + $maskBuilder = new MaskBuilder(); + + foreach ($attributes as $attribute) { + $maskBuilder->add($attribute); + } + + return $maskBuilder->get(); + } } diff --git a/Domain/AclManager.php b/Domain/AclManager.php index 41c5395..343a6f2 100644 --- a/Domain/AclManager.php +++ b/Domain/AclManager.php @@ -2,7 +2,9 @@ namespace Problematic\AclManagerBundle\Domain; -use Symfony\Component\Security\Acl\Exception\NoAceFoundException; +use Symfony\Component\Security\Acl\Model\ObjectIdentityInterface; +use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface; +use Symfony\Component\Security\Acl\Voter\FieldVote; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; use Symfony\Component\Security\Core\Role\RoleInterface; @@ -33,9 +35,9 @@ public function addClassPermission($domainObject, $mask, $securityIdentity = nul /** * {@inheritDoc} */ - public function addObjectFieldPermission($domainObject, $field, $mask, $securityIdentity = null) + public function addObjectFieldPermission($domainObject, $fields, $mask, $securityIdentity = null) { - $this->addPermission($domainObject, $field, $mask, $securityIdentity, 'object', false); + $this->addPermission($domainObject, $fields, $mask, $securityIdentity, 'object', false); return $this; } @@ -43,37 +45,37 @@ public function addObjectFieldPermission($domainObject, $field, $mask, $security /** * {@inheritDoc} */ - public function addClassFieldPermission($domainObject, $field, $mask, $securityIdentity = null) + public function addClassFieldPermission($domainObject, $fields, $mask, $securityIdentity = null) { - $this->addPermission($domainObject, $field, $mask, $securityIdentity, 'class', false); + $this->addPermission($domainObject, $fields, $mask, $securityIdentity, 'class', false); return $this; } /** * @param mixed $domainObject - * @param string $field - * @param int $mask + * @param string $fields * @param UserInterface|TokenInterface|RoleInterface $securityIdentity * @param string $type - * @param string $field - * @param boolean $replace_existing + * @param string $fields + * @param boolean $replaceExisting + * * @return AbstractAclManager */ - protected function addPermission($domainObject, $field, $mask, $securityIdentity = null, $type = 'object', $replace_existing = false) + protected function addPermission($domainObject, $fields, $attributes, $securityIdentity = null, $type = 'object', $replaceExisting = false) { - if (is_null($securityIdentity)) { - $securityIdentity = $this->getUser(); - } + $mask = $this->buildMask($attributes); - $context = $this->doCreatePermissionContext($type, $field, $securityIdentity, $mask); + $context = $this->doCreatePermissionContext( + $type, + $fields, + $this->doCreateSecurityIdentity($securityIdentity), + $mask + ); - $objectIdentityRetriever = $this->getObjectIdentityRetrievalStrategy(); - $objectIdentityRetriever->setType($type); - $oid = $objectIdentityRetriever->getObjectIdentity($domainObject); + $acl = $this->doLoadAcl($this->doRetrieveObjectIdentity($domainObject, $type)); - $acl = $this->doLoadAcl($oid); - $this->doApplyPermission($acl, $context, $replace_existing); + $this->doApplyPermission($acl, $context, $replaceExisting); $this->getAclProvider()->updateAcl($acl); @@ -85,12 +87,12 @@ protected function addPermission($domainObject, $field, $mask, $securityIdentity * @param int $mask * @param UserInterface | TokenInterface | RoleInterface $securityIdentity * @param string $type - * @param string $field + * @param string $fields * @return \Problematic\AclManagerBundle\Domain\AbstractAclManager */ - protected function setPermission($domainObject, $field, $mask, $securityIdentity = null, $type = 'object') + protected function setPermission($domainObject, $fields, $mask, $securityIdentity = null, $type = 'object') { - $this->addPermission($domainObject, $field, $mask, $securityIdentity, $type, true); + $this->addPermission($domainObject, $fields, $mask, $securityIdentity, $type, true); return $this; } @@ -114,35 +116,36 @@ public function setClassPermission($domainObject, $mask, $securityIdentity = nul /** * {@inheritDoc} */ - public function setObjectFieldPermission($domainObject, $field, $mask, $securityIdentity = null) + public function setObjectFieldPermission($domainObject, $fields, $mask, $securityIdentity = null) { - $this->setPermission($domainObject, $field, $mask, $securityIdentity, 'object'); + $this->setPermission($domainObject, $fields, $mask, $securityIdentity, 'object'); } /** * {@inheritDoc} */ - public function setClassFieldPermission($domainObject, $field, $mask, $securityIdentity = null) + public function setClassFieldPermission($domainObject, $fields, $mask, $securityIdentity = null) { - $this->setPermission($domainObject, $field, $mask, $securityIdentity, 'class'); + $this->setPermission($domainObject, $fields, $mask, $securityIdentity, 'class'); } /** * {@inheritDoc} */ - public function revokePermission($domainObject, $mask, $securityIdentity = null, $type = 'object') + public function revokePermission($domainObject, $attributes, $securityIdentity = null, $type = 'object') { - if (is_null($securityIdentity)) { - $securityIdentity = $this->getUser(); + if (null !== $securityIdentity) { + $securityIdentity = $this->doCreateSecurityIdentity($securityIdentity); } - $context = $this->doCreatePermissionContext($type, null, $securityIdentity, $mask); - - $objectIdentityRetriever = $this->getObjectIdentityRetrievalStrategy(); - $objectIdentityRetriever->setType($type); - $oid = $objectIdentityRetriever->getObjectIdentity($domainObject); + $context = $this->doCreatePermissionContext( + $type, + null, + $securityIdentity, + $this->buildMask($attributes) + ); - $acl = $this->doLoadAcl($oid); + $acl = $this->doLoadAcl($this->doRetrieveObjectIdentity($domainObject, $type)); $this->doRevokePermission($acl, $context); $this->getAclProvider()->updateAcl($acl); @@ -152,19 +155,21 @@ public function revokePermission($domainObject, $mask, $securityIdentity = null, /** * {@inheritDoc} */ - public function revokeFieldPermission($domainObject, $field, $mask, $securityIdentity = null, $type = 'object') + public function revokeFieldPermission($domainObject, $fields, $attributes, $securityIdentity = null, $type = 'object') { - if (is_null($securityIdentity)) { - $securityIdentity = $this->getUser(); + if (null === $securityIdentity) { + $securityIdentity = $this->doCreateSecurityIdentity($securityIdentity); } - $context = $this->doCreatePermissionContext($type, $field, $securityIdentity, $mask); + $context = $this->doCreatePermissionContext( + $type, + $fields, + $securityIdentity, + $this->buildMask($attributes) + ); - $objectIdentityRetriever = $this->getObjectIdentityRetrievalStrategy(); - $objectIdentityRetriever->setType($type); - $oid = $objectIdentityRetriever->getObjectIdentity($domainObject); + $acl = $this->doLoadAcl($this->doRetrieveObjectIdentity($domainObject, $type)); - $acl = $this->doLoadAcl($oid); $this->doRevokePermission($acl, $context); $this->getAclProvider()->updateAcl($acl); @@ -190,41 +195,42 @@ public function revokeAllObjectPermissions($domainObject, $securityIdentity = nu /** * {@inheritDoc} */ - public function revokeAllClassFieldPermissions($domainObject, $field, $securityIdentity = null) + public function revokeAllClassFieldPermissions($domainObject, $fields, $securityIdentity = null) { - $this->revokeAllPermissions($domainObject, $field, $securityIdentity, 'class'); + $this->revokeAllPermissions($domainObject, $fields, $securityIdentity, 'class'); } /** * {@inheritDoc} */ - public function revokeAllObjectFieldPermissions($domainObject, $field, $securityIdentity = null) + public function revokeAllObjectFieldPermissions($domainObject, $fields, $securityIdentity = null) { - $this->revokeAllPermissions($domainObject, $field, $securityIdentity, 'object'); + $this->revokeAllPermissions($domainObject, $fields, $securityIdentity, 'object'); } /** * @param mixed $domainObject - * @param string $field - * @param null $securityIdentity + * @param string|string[] $fields + * @param null|string|SecurityIdentityInterface|UserInterface $securityIdentity * @param string $type * * @return $this */ - protected function revokeAllPermissions($domainObject, $field, $securityIdentity = null, $type = 'object') + protected function revokeAllPermissions($domainObject, $fields, $securityIdentity = null, $type = 'object') { - if (is_null($securityIdentity)) { - $securityIdentity = $this->getUser(); + if (null !== $securityIdentity) { + $securityIdentity = $this->doCreateSecurityIdentity($securityIdentity); } - $securityIdentity = $this->doCreateSecurityIdentity($securityIdentity); - - $objectIdentityRetriever = $this->getObjectIdentityRetrievalStrategy(); - $objectIdentityRetriever->setType($type); - $oid = $objectIdentityRetriever->getObjectIdentity($domainObject); + $context = $this->doCreatePermissionContext( + $type, + $fields, + $securityIdentity + ); + $oid = $this->doRetrieveObjectIdentity($domainObject, $type); $acl = $this->doLoadAcl($oid); - $this->doRevokeAllPermissions($acl, $securityIdentity, $type, $field); + $this->doRevokeAllPermissions($acl, $context); $this->getAclProvider()->updateAcl($acl); return $this; @@ -249,7 +255,7 @@ public function preloadAcls($objects, $identities = array()) $sids[] = $sid; } - $acls = $this->getAclProvider()->findAcls($oids, $sids); // todo: do we need to do anything with these? + $acls = $this->getAclProvider()->findAcls($oids, $sids); return $acls; } @@ -257,13 +263,9 @@ public function preloadAcls($objects, $identities = array()) /** * {@inheritDoc} */ - public function deleteAclFor($managedItem, $type = 'class') + public function deleteAclFor($object, $type = 'object') { - $objectIdentityRetriever = $this->getObjectIdentityRetrievalStrategy(); - $objectIdentityRetriever->setType($type); - - $oid = $objectIdentityRetriever->getObjectIdentity($managedItem); - $this->getAclProvider()->deleteAcl($oid); + $this->getAclProvider()->deleteAcl($this->doRetrieveObjectIdentity($object, $type)); return $this; } @@ -271,26 +273,46 @@ public function deleteAclFor($managedItem, $type = 'class') /** * {@inheritDoc} */ - public function isGranted($attributes, $object = null) + public function isGranted($attributes, $object = null, $type = 'object') { - return $this->getSecurityContext()->isGranted($attributes, $object); + return $this->getSecurityContext()->isGranted( + $attributes, + $this->doRetrieveObjectIdentity($object, $type) + ); } /** * {@inheritDoc} */ - public function isFieldGranted($masks, $object, $field) + public function isFieldGranted($attributes, $object, $fields, $type = 'object') { - $oid = $this->getObjectIdentityRetrievalStrategy()->getObjectIdentity($object); - $acl = $this->doLoadAcl($oid); + if (!is_array($fields)) { + $fields = array($fields); + } - try { - return $acl->isFieldGranted($field, $masks, array( - $this->doCreateSecurityIdentity($this->getUser()), - )); - } catch (NoAceFoundException $ex) { - return false; + $oid = $this->doRetrieveObjectIdentity($object, $type); + $fieldGranted = array(); + + foreach ($fields as $field) { + if (true === $this->getSecurityContext()->isGranted($attributes, new FieldVote($oid, $field))) { + $fieldGranted[$field] = true; + } } + + return count($fields) === count($fieldGranted); + } + + protected function doRetrieveObjectIdentity($object, $type) + { + if ($object instanceof ObjectIdentityInterface) { + $oid = $object; + } else { + $objectIdentityRetriever = $this->getObjectIdentityRetrievalStrategy(); + $objectIdentityRetriever->setType($type); + $oid = $objectIdentityRetriever->getObjectIdentity($object); + } + + return $oid; } /** @@ -304,8 +326,10 @@ public function getUser() return; } - $user = $token->getUser(); + if (false === $token->isAuthenticated()) { + return AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY; + } - return (is_object($user)) ? $user : AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY; + return $token->getUser(); } } diff --git a/Domain/PermissionContext.php b/Domain/PermissionContext.php index 39892a9..43be808 100644 --- a/Domain/PermissionContext.php +++ b/Domain/PermissionContext.php @@ -8,15 +8,30 @@ class PermissionContext implements PermissionContextInterface { + /** + * @var int + */ protected $permissionMask; + + /** + * @var SecurityIdentityInterface + */ protected $securityIdentity; + + /** + * @var string + */ protected $permissionType; - protected $field; - protected $granting; - public function __construct() - { - } + /** + * @var array + */ + protected $fields; + + /** + * @var bool + */ + protected $granting; /** * @param integer $mask permission mask, or null for all @@ -31,7 +46,7 @@ public function getMask() return $this->permissionMask; } - public function setSecurityIdentity(SecurityIdentityInterface $securityIdentity) + public function setSecurityIdentity(SecurityIdentityInterface $securityIdentity = null) { $this->securityIdentity = $securityIdentity; } @@ -61,15 +76,16 @@ public function isGranting() return $this->granting; } - public function setField($field) + public function setFields(array $fields) { - $this->field = $field; + $this->fields = $fields; } - public function getField() + public function getFields() { - return $this->field; + return $this->fields; } + public function equals(AuditableEntryInterface $ace) { return $ace->getSecurityIdentity() == $this->getSecurityIdentity() && diff --git a/Model/AclManagerInterface.php b/Model/AclManagerInterface.php index 9beae17..5db7419 100644 --- a/Model/AclManagerInterface.php +++ b/Model/AclManagerInterface.php @@ -3,6 +3,8 @@ namespace Problematic\AclManagerBundle\Model; use Symfony\Component\Security\Acl\Model\DomainObjectInterface; +use Symfony\Component\Security\Acl\Model\MutableAclProviderInterface; +use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Role\RoleInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -14,8 +16,8 @@ interface AclManagerInterface * user and this object will be over written. If none existed, a new one will be created. * * @param mixed $domainObject - * @param int $mask - * @param UserInterface|TokenInterface|RoleInterface $securityIdentity if none given, the current session user will be used + * @param int|string|string[] $mask + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface $securityIdentity if none given, the current session user will be used * @return self */ public function addObjectPermission($domainObject, $mask, $securityIdentity = null); @@ -25,8 +27,8 @@ public function addObjectPermission($domainObject, $mask, $securityIdentity = nu * user and this class will be over written. If none existed, a new one will be created. * * @param mixed $domainObject - * @param int $mask - * @param UserInterface|TokenInterface|RoleInterface $securityIdentity if none given, the current session user will be used + * @param int|string|string[] $mask + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity if none given, the current session user will be used * @return self */ public function addClassPermission($domainObject, $mask, $securityIdentity = null); @@ -35,31 +37,31 @@ public function addClassPermission($domainObject, $mask, $securityIdentity = nul * for this user and this object will be over written. If none existed, a new one will be created. * * @param mixed $domainObject - * @param string $field - * @param int $mask - * @param UserInterface|TokenInterface|RoleInterface $securityIdentity if none fiven, the current session user will be used + * @param string $fields + * @param int|string|string[] $mask + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity if none fiven, the current session user will be used * @return self */ - public function addObjectFieldPermission($domainObject, $field, $mask, $securityIdentity = null); + public function addObjectFieldPermission($domainObject, $fields, $mask, $securityIdentity = null); /** Set permission mask for a given field of a class. All previous permissions for this * user and this object will be over written. If none existed, a new one will be created. * * @param mixed $domainObject - * @param string $field - * @param int $mask - * @param UserInterface|TokenInterface|RoleInterface $securityIdentity if none fiven, the current session user will be used + * @param string|string[] $fields + * @param int|string|string[] $mask + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null if none given, the current session user will be used * @return self */ - public function addClassFieldPermission($domainObject, $field, $mask, $securityIdentity = null); + public function addClassFieldPermission($domainObject, $fields, $mask, $securityIdentity = null); /** * Sets permission mask for a given domain object. All previous permissions for this * user and this object will be over written. If none existed, a new one will be created. * * @param mixed $domainObject - * @param int $mask - * @param UserInterface|TokenInterface|RoleInterface $securityIdentity if none given, the current session user will be used + * @param int|string|string[] $mask + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity if none given, the current session user will be used */ public function setObjectPermission($domainObject, $mask, $securityIdentity = null); @@ -68,8 +70,8 @@ public function setObjectPermission($domainObject, $mask, $securityIdentity = nu * user and this class will be over written. If none existed, a new one will be created. * * @param mixed $domainObject - * @param int $mask - * @param UserInterface|TokenInterface|RoleInterface $securityIdentity if none given, the current session user will be used + * @param int|string|string[] $mask + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity if none given, the current session user will be used */ public function setClassPermission($domainObject, $mask, $securityIdentity = null); @@ -77,68 +79,66 @@ public function setClassPermission($domainObject, $mask, $securityIdentity = nul * for this user and this object will be over written. If none existed, a new one will be created. * * @param mixed $domainObject - * @param string $field - * @param int $mask - * @param UserInterface|TokenInterface|RoleInterface $securityIdentity if none fiven, the current session user will be used + * @param string|string[] $fields + * @param int|string|string[] $mask + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity if none fiven, the current session user will be used */ - public function setObjectFieldPermission($domainObject, $field, $mask, $securityIdentity = null); + public function setObjectFieldPermission($domainObject, $fields, $mask, $securityIdentity = null); /** Set permission mask for a given field of a class. All previous permissions for this * user and this object will be over written. If none existed, a new one will be created. * * @param mixed $domainObject - * @param string $field - * @param int $mask - * @param UserInterface|TokenInterface|RoleInterface $securityIdentity if none fiven, the current session user will be used + * @param string|string[] $fields + * @param int|string|string[] $mask + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity if none fiven, the current session user will be used */ - public function setClassFieldPermission($domainObject, $field, $mask, $securityIdentity = null); + public function setClassFieldPermission($domainObject, $fields, $mask, $securityIdentity = null); /** * @param mixed $domainObject - * @param int $mask - * @param null $securityIdentity + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity * @param string $type * * @return self */ - public function revokePermission($domainObject, $mask, $securityIdentity = null, $type = 'object'); + public function revokePermission($domainObject, $attributes, $securityIdentity = null, $type = 'object'); /** * @param mixed $domainObject - * @param string $field - * @param int $mask - * @param null $securityIdentity + * @param string|string[] $fields + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity * @param string $type * * @return self */ - public function revokeFieldPermission($domainObject, $field, $mask, $securityIdentity = null, $type = 'object'); + public function revokeFieldPermission($domainObject, $fields, $attributes, $securityIdentity = null, $type = 'object'); /** * @param mixed $domainObject - * @param UserInterface | TokenInterface | RoleInterface $securityIdentity if none given, the current session user will be used + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity if none given, the current session user will be used */ public function revokeAllObjectPermissions($domainObject, $securityIdentity = null); /** * @param mixed $domainObject - * @param UserInterface | TokenInterface | RoleInterface $securityIdentity if none given, the current session user will be used + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface if none given, the current session user will be used */ public function revokeAllClassPermissions($domainObject, $securityIdentity = null); /** * @param mixed $domainObject - * @param string $field - * @param UserInterface | TokenInterface | RoleInterface $securityIdentity if none given, the current session user will be used + * @param string|string[] $fields + * @param string|UserInterface|TokenInterface|RoleInterface|SecurityIdentityInterface|null $securityIdentity if none given, the current session user will be used */ - public function revokeAllObjectFieldPermissions($domainObject, $field, $securityIdentity = null); + public function revokeAllObjectFieldPermissions($domainObject, $fields, $securityIdentity = null); /** * @param mixed $domainObject - * @param string $field + * @param string|string[] $fields * @param UserInterface | TokenInterface | RoleInterface $securityIdentity if none given, the current session user will be used */ - public function revokeAllClassFieldPermissions($domainObject, $field, $securityIdentity = null); + public function revokeAllClassFieldPermissions($domainObject, $fields, $securityIdentity = null); /** * Pre Load Acls for all managed entries, that avoid doctrine to create N extra request. @@ -153,28 +153,29 @@ public function preloadAcls($objects, $identities = array()); /** * Delete entry related of item managed via ACL system * - * @param string|\DomainObject|DomainObjectInterface $managedItem + * @param string|DomainObjectInterface $managedItem * * @return self */ - public function deleteAclFor($managedItem, $type = 'class'); + public function deleteAclFor($managedItem, $type = 'object'); /** * @param string|string[] $attributes * @param null|object $object + * @param string $type * * @return bool */ - public function isGranted($attributes, $object = null); + public function isGranted($attributes, $object = null, $type = 'object'); /** - * @param string|string[] $masks * @param object $object - * @param string $field + * @param string|string[] $fields + * @param string $type * * @return bool */ - public function isFieldGranted($attributes, $object, $field); + public function isFieldGranted($attributes, $object, $fields, $type = 'object'); /** * Retrieves the current session user @@ -182,4 +183,9 @@ public function isFieldGranted($attributes, $object, $field); * @return UserInterface */ public function getUser(); + + /** + * @return MutableAclProviderInterface + */ + public function getAclProvider(); } diff --git a/Model/PermissionContextInterface.php b/Model/PermissionContextInterface.php index a507bb5..3dd7a05 100644 --- a/Model/PermissionContextInterface.php +++ b/Model/PermissionContextInterface.php @@ -23,9 +23,9 @@ public function getSecurityIdentity(); public function getPermissionType(); /** - * @return string + * @return array */ - public function getField(); + public function getFields(); /** * @return bool diff --git a/ORM/AclFilter.php b/ORM/AclFilter.php index e1bf6a4..c42b970 100644 --- a/ORM/AclFilter.php +++ b/ORM/AclFilter.php @@ -54,22 +54,21 @@ public function apply( $identity = $token->getUser(); } - if(!is_array($extraCriteria)){ + if (!is_array($extraCriteria)) { $extraCriteria = array($extraCriteria); } $sqlQueries = []; - foreach($extraCriteria as $criteria){ - if($criteria instanceof QueryBuilder) { + foreach ($extraCriteria as $criteria) { + if ($criteria instanceof QueryBuilder) { $sqlQueries[] = $criteria->getQuery()->getSQL(); - } elseif($criteria instanceof Query){ + } elseif ($criteria instanceof Query) { $sqlQueries[] = $criteria->getSQL(); - } else{ + } else { $sqlQueries[] = $criteria; } } - $query->setHint(static::HINT_ACL_EXTRA_CRITERIA, $sqlQueries); if ($query instanceof QueryBuilder) { @@ -99,8 +98,7 @@ public function apply( $hintAclMetadata = (false !== $query->getHint('acl.metadata')) ? $query->getHint('acl.metadata') - : array() - ; + : array(); $hintAclMetadata[] = array('query' => $aclQuery, 'table' => $table, 'alias' => $alias); @@ -116,7 +114,7 @@ public function apply( * @param array $classes * @param array $identifiers * @param integer $mask - * @param array $extraCriteria + * @param array $extraCriteria * @return string */ private function getExtraQuery(Array $classes, Array $identifiers, $mask) diff --git a/ORM/AclWalker.php b/ORM/AclWalker.php index 92fc3fb..9c2113f 100644 --- a/ORM/AclWalker.php +++ b/ORM/AclWalker.php @@ -9,7 +9,7 @@ class AclWalker extends SqlWalker /** * @param $fromClause * - * @return string + * @return string */ public function walkFromClause($fromClause) { @@ -37,7 +37,7 @@ public function walkFromClause($fromClause) } /** - * @param array $extraQueries + * @param array $extraQueries * @param string $tableAlias * * @return array @@ -46,8 +46,8 @@ protected function parseExtraQueries(Array $extraQueries, $tableAlias) { $clause = array(); - foreach($extraQueries as $query){ - $clause[] = $tableAlias.'.id IN(('.$query.'))'; + foreach ($extraQueries as $query) { + $clause[] = $tableAlias . '.id IN((' . $query . '))'; } return implode(' OR ', $clause); diff --git a/RetrievalStrategy/AclObjectRetrievalStrategy.php b/RetrievalStrategy/AclObjectRetrievalStrategy.php index 6ae2ba9..b176303 100644 --- a/RetrievalStrategy/AclObjectRetrievalStrategy.php +++ b/RetrievalStrategy/AclObjectRetrievalStrategy.php @@ -4,6 +4,7 @@ use Symfony\Component\Security\Acl\Domain\ObjectIdentity; use Symfony\Component\Security\Acl\Domain\ObjectIdentityRetrievalStrategy; +use Symfony\Component\Security\Core\Util\ClassUtils; /** * @author Johann Saunier @@ -24,17 +25,29 @@ public function setType($type) } /** - * @param object $domainObject + * @param object $object * - * @return ObjectIdentity|\Symfony\Component\Security\Acl\Model\ObjectIdentityInterface + * @return ObjectIdentity|\Symfony\Component\Security\Acl\Model\ObjectIdentityInterface|void + * @throws \Exception */ - public function getObjectIdentity($domainObject) + public function getObjectIdentity($object) { - //We allowed to retrieve objectIdentity from string ! - if (is_string($domainObject)) { - return new ObjectIdentity($this->type, $domainObject); + if ('class' === $this->type) { + if (is_object($object)) { + return new ObjectIdentity(ClassUtils::getRealClass($object), $this->type); + } + + if (is_string($object)) { + return new ObjectIdentity($object, $this->type); + } + + throw new \Exception('Undefined type, can\'t retrieve oid'); + } + + if ('object' === $this->type) { + return parent::getObjectIdentity($object); } - return parent::getObjectIdentity($domainObject); + throw new \Exception('Unknown type'); } } diff --git a/Tests/App/AppKernel.php b/Tests/App/AppKernel.php new file mode 100644 index 0000000..bd75216 --- /dev/null +++ b/Tests/App/AppKernel.php @@ -0,0 +1,24 @@ +load(__DIR__ . '/config/config.yml'); + } +} diff --git a/Tests/App/config/config.yml b/Tests/App/config/config.yml new file mode 100644 index 0000000..88df73e --- /dev/null +++ b/Tests/App/config/config.yml @@ -0,0 +1,50 @@ +framework: + translator: { fallback: en } + secret: secret + router: + resource: "%kernel.root_dir%/config/routing.yml" + strict_requirements: %kernel.debug% + templating: + engines: ['php'] + default_locale: en + session: ~ + test: ~ + trusted_hosts: ~ + +security: + acl: + connection: default + + providers: + in_memory: + memory: ~ + + firewalls: + main: + anonymous: true + pattern: ^/ + + encoders: + Symfony\Component\Security\Core\User\User: plaintext + +#Use for CI +doctrine: + dbal: + connections: + default: + driver: pdo_sqlite + memory: true + +#Use real db to debug +#doctrine: +# dbal: +# default_connection: default +# connections: +# default: +# driver: pdo_mysql +# host: 127.0.0.1 +# port: 3306 +# dbname: test_acl +# user: root +# password: root +# charset: "UTF8" diff --git a/Tests/Domain/AclManagerTest.php b/Tests/Domain/AclManagerTest.php new file mode 100644 index 0000000..bc7fb80 --- /dev/null +++ b/Tests/Domain/AclManagerTest.php @@ -0,0 +1,448 @@ +fooClass = 'Problematic\\AclManagerBundle\\Tests\\Model\\FooObject'; + $this->barClass = 'Problematic\\AclManagerBundle\\Tests\\Model\\BarObject'; + } + + public function testGetProvider() + { + $this->assertInstanceOf( + 'Symfony\Component\Security\Core\User\UserInterface', + $this->aclManager->getUser(), + 'Must retrieve UserInterface' + ); + } + + public function testIsGrantedObject() + { + $a = new FooObject(uniqid()); + $b = new BarObject(uniqid()); + + $user1Sid = $this->generateSidForUser('user1'); + $user2Sid = $this->generateSidForUser('user2'); + + $this->aclManager + ->addObjectPermission($b, 'OWNER', $user1Sid) + ->addObjectPermission($a, 'VIEW', $user1Sid); + + $this->aclManager + ->addObjectPermission($b, 'VIEW', $user2Sid) + ->addObjectPermission($a, 'OWNER', $user2Sid); + + $this->authenticateUser('user1'); + $this->assertTrue($this->aclManager->isGranted('OWNER', $b)); + $this->assertTrue($this->aclManager->isGranted('VIEW', $a)); + $this->assertFalse($this->aclManager->isGranted('OWNER', $a)); + + $this->authenticateUser('user2'); + $this->assertTrue($this->aclManager->isGranted('OWNER', $a)); + $this->assertTrue($this->aclManager->isGranted('VIEW', $b)); + $this->assertFalse($this->aclManager->isGranted('OWNER', $b)); + + $this->authenticateUser('sneakyuser'); + $this->assertFalse($this->aclManager->isGranted('DELETE', $a)); + $this->assertFalse($this->aclManager->isGranted('VIEW', $a)); + $this->assertFalse($this->aclManager->isGranted('DELETE', $b)); + $this->assertFalse($this->aclManager->isGranted('VIEW', $b)); + } + + public function testIsFieldGrantedClass() + { + $a = new FooObject(uniqid()); + $b = new FooObject(uniqid()); + $c = new FooObject(uniqid()); + $d = new BarObject(uniqid()); + + $this->aclManager + ->addClassFieldPermission($a, 'securedField', 'MASTER', 'ROLE_ADMIN') + ->addClassFieldPermission($c, 'securedField', 'VIEW', $this->generateSidForUser('user1')) + ->addClassFieldPermission($this->fooClass, ['securedField', 'bar'], 'VIEW', $this->generateSidForUser('user2')); + + $this->authenticateUser('admin', ['ROLE_ADMIN']); + $this->assertTrue($this->aclManager->isFieldGranted('MASTER', $a, 'securedField', 'class')); + $this->assertTrue($this->aclManager->isFieldGranted('MASTER', $b, 'securedField', 'class')); + $this->assertTrue($this->aclManager->isFieldGranted('MASTER', $c, 'securedField', 'class')); + $this->assertFalse($this->aclManager->isFieldGranted('MASTER', $c, 'foo', 'class')); + $this->assertFalse($this->aclManager->isFieldGranted('MASTER', $c, 'bar', 'class')); + + $this->authenticateUser('user1'); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $a, 'securedField', 'class')); + + $this->authenticateUser('user2'); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $a, 'securedField', 'class')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $a, 'bar', 'class')); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $d, 'securedField', 'class')); + + $this->authenticateUser('sneakyuser'); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $a, 'securedField', 'class')); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $b, 'securedField', 'class')); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $c, 'securedField', 'class')); + } + + public function testIsGrantedClass() + { + $a = new FooObject(uniqid()); + $b = new BarObject(uniqid()); + + $user3Sid = $this->generateSidForUser('user3'); + + $this->aclManager + ->addClassPermission($a, 'EDIT', $user3Sid) + ->addClassPermission($this->barClass, 'VIEW', $user3Sid); + + $this->authenticateUser('user3'); + $this->assertTrue($this->aclManager->isGranted('EDIT', $a, 'class')); + $this->assertTrue($this->aclManager->isGranted('EDIT', new ObjectIdentity($this->fooClass, 'class'))); + $this->assertTrue($this->aclManager->isGranted('EDIT', $a, 'class')); + $this->assertTrue($this->aclManager->isGranted('EDIT', get_class($a), 'class')); + $this->assertTrue($this->aclManager->isGranted('VIEW', $b, 'class')); + + $this->authenticateUser('sneakyuser'); + $this->assertFalse($this->aclManager->isGranted('OWNER', $a, 'class')); + $this->assertFalse($this->aclManager->isGranted('OWNER', get_class($a), 'class')); + $this->assertFalse($this->aclManager->isGranted('VIEW', $b, 'class')); + } + + public function testIsGrantedRoles() + { + $this->authenticateUser('user1'); + $this->assertTrue($this->aclManager->isGranted('ROLE_USER')); + $this->assertFalse($this->aclManager->isGranted('ROLE_ADMIN')); + + $this->authenticateUser('user2'); + $this->assertTrue($this->aclManager->isGranted('ROLE_USER')); + $this->assertFalse($this->aclManager->isGranted('ROLE_ADMIN')); + + $this->authenticateUser('admin', ['ROLE_ADMIN']); + $this->assertTrue($this->aclManager->isGranted('ROLE_USER')); + $this->assertTrue($this->aclManager->isGranted('ROLE_ADMIN')); + $this->assertTrue($this->aclManager->isGranted(array('ROLE_ADMIN', 'ROLE_USER'))); + } + + public function testRevokePermission() + { + $a = new FooObject('revoke_permission_object_a' . uniqid()); + $b = new FooObject('revoke_permission_object_b' . uniqid()); + + $user3Sid = $this->generateSidForUser('user3'); + $user4Sid = $this->generateSidForUser('user4'); + + $this->aclManager + ->addObjectPermission($a, 'OWNER', $user3Sid) + ->addObjectPermission($b, 'VIEW', $user3Sid) + ->addObjectPermission($a, 'VIEW', $user4Sid) + ->addObjectPermission($b, 'OWNER', $user4Sid); + + //Revoke permission for user4 + $this->aclManager->revokePermission($a, 'EDIT', $user4Sid); + + $this->authenticateUser('user4'); + $this->assertFalse($this->aclManager->isGranted('EDIT', $a)); + $this->assertFalse($this->aclManager->isGranted('OWNER', $a)); + $this->assertTrue($this->aclManager->isGranted('VIEW', $a)); + $this->assertTrue($this->aclManager->isGranted('OWNER', $b)); + $this->assertTrue($this->aclManager->isGranted('VIEW', $b)); + + $this->authenticateUser('user3'); + $this->assertTrue($this->aclManager->isGranted('OWNER', $a)); + + //Revoke permission for all users + $this->aclManager->revokePermission($a, 'VIEW'); + } + + public function testRevokeFieldPermission() + { + $a = new FooObject(uniqid()); + $b = new FooObject(uniqid()); + + $user5Sid = $this->generateSidForUser('user5'); + $user6Sid = $this->generateSidForUser('user6'); + + $this->aclManager + ->addObjectFieldPermission($a, 'securedField', 'OWNER', $user5Sid) + ->addObjectFieldPermission($a, 'foo', 'VIEW', $user5Sid) + ->addObjectFieldPermission($b, 'securedField', 'VIEW', $user5Sid) + ->addObjectFieldPermission($a, 'securedField', 'VIEW', $user6Sid) + ->addObjectFieldPermission($b, 'securedField', 'OWNER', $user6Sid) + ->addObjectFieldPermission($b, 'foo', 'VIEW', $user6Sid); + + $this->authenticateUser('user5'); + $this->assertTrue($this->aclManager->isFieldGranted('OWNER', $a, 'securedField')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $a, 'foo')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, 'securedField')); + $this->assertFalse($this->aclManager->isFieldGranted('IDDQ', $a, 'securedField')); + + $this->authenticateUser('user6'); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $a, 'securedField')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, 'foo')); + $this->assertTrue($this->aclManager->isFieldGranted('OWNER', $b, 'securedField')); + $this->assertFalse($this->aclManager->isFieldGranted('IDDQ', $a, 'securedField')); + + $this->aclManager->revokeFieldPermission($a, 'securedField', 'OWNER', $user5Sid); + + $this->authenticateUser('user5'); + $this->assertFalse($this->aclManager->isFieldGranted('OWNER', $a, 'securedField')); + $this->assertFalse($this->aclManager->isFieldGranted('EDIT', $a, 'securedField')); + + $this->authenticateUser('user6'); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $a, 'securedField')); + $this->assertFalse($this->aclManager->isFieldGranted('IDDQ', $a, 'securedField')); + + $this->aclManager->revokeFieldPermission($b, ['foo', 'securedField'], 'VIEW', $user6Sid); + + $this->authenticateUser('user6'); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $b, 'foo')); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $b, 'securedField')); + + $this->authenticateUser('user5'); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, 'securedField')); + } + + public function testRevokeAllObjectPermissions() + { + $a = new FooObject(uniqid()); + $b = new FooObject(uniqid()); + + $user7Sid = $this->generateSidForUser('user7'); + $user8Sid = $this->generateSidForUser('user8'); + + $this->aclManager + ->addObjectPermission($a, 'OWNER', $user7Sid) + ->addObjectPermission($a, 'VIEW', $user8Sid) + ->addObjectPermission($b, 'OWNER', $user7Sid) + ->addObjectPermission($b, 'VIEW', $user8Sid); + + //Delete permission for all SID + $this->aclManager->revokeAllObjectPermissions($a); + + $this->authenticateUser('user7'); + $this->assertFalse($this->aclManager->isGranted('OWNER', $a)); + $this->assertTrue($this->aclManager->isGranted('OWNER', $b)); + + $this->authenticateUser('user8'); + $this->assertFalse($this->aclManager->isGranted('VIEW', $a)); + $this->assertTrue($this->aclManager->isGranted('VIEW', $b)); + + //Delete permission only for user8 + $this->aclManager->revokeAllObjectPermissions($b, $user8Sid); + + $this->authenticateUser('user7'); + $this->assertTrue($this->aclManager->isGranted('OWNER', $b)); + + $this->authenticateUser('user8'); + $this->assertFalse($this->aclManager->isGranted('VIEW', $b)); + } + + public function testRevokeAllObjectFieldPermissions() + { + $a = new FooObject(uniqid()); + $b = new FooObject(uniqid()); + + $user9Sid = $this->generateSidForUser('user9'); + $user10Sid = $this->generateSidForUser('user10'); + + $this->aclManager + ->addObjectFieldPermission($a, 'securedField', 'OWNER', $user9Sid) + ->addObjectFieldPermission($a, 'foo', 'OWNER', $user9Sid) + ->addObjectFieldPermission($a, 'bar', 'OWNER', $user9Sid) + ->addObjectFieldPermission($a, 'securedField', 'VIEW', $user10Sid) + ->addObjectFieldPermission($a, 'bar', 'VIEW', $user10Sid) + ->addObjectFieldPermission($a, 'foo', 'VIEW', $user10Sid) + ->addObjectFieldPermission($b, 'securedField', 'OWNER', $user9Sid) + ->addObjectFieldPermission($b, 'foo', 'OWNER', $user9Sid) + ->addObjectFieldPermission($b, 'securedField', 'VIEW', $user10Sid) + ->addObjectFieldPermission($b, 'foo', 'VIEW', $user10Sid) + ->addObjectFieldPermission($b, 'bar', 'VIEW', $user10Sid) + ->addObjectFieldPermission($b, 'bar', 'VIEW', $user9Sid); + + //Revoke all field permission for all sid + $this->aclManager->revokeAllObjectFieldPermissions($a, 'securedField'); + + $this->authenticateUser('user9'); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $a, 'securedField')); + $this->assertTrue($this->aclManager->isFieldGranted('OWNER', $a, 'foo')); + + $this->authenticateUser('user10'); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $a, 'securedField')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $a, 'foo')); + + //Revoke all field permission for all sid + $this->aclManager->revokeAllObjectFieldPermissions($a, array('foo', 'bar')); + + $this->authenticateUser('user9'); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $a, 'foo')); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $a, 'bar')); + $this->assertTrue($this->aclManager->isFieldGranted('OWNER', $b, 'securedField')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, array('foo', 'bar'))); + + $this->authenticateUser('user10'); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $a, 'foo')); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $a, 'bar')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, 'securedField')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, array('foo', 'bar'))); + + //Revoke all field permission only for user10 + $this->aclManager->revokeAllObjectFieldPermissions($b, array('foo', 'bar'), $user10Sid); + + $this->authenticateUser('user9'); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, 'foo')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, 'bar')); + $this->assertTrue($this->aclManager->isFieldGranted('VIEW', $b, array('bar', 'foo'))); + + $this->authenticateUser('user10'); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $b, 'foo')); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $b, 'bar')); + $this->assertFalse($this->aclManager->isFieldGranted('VIEW', $b, array('bar', 'foo'))); + } + + public function testRevokeClassPermissions() + { + $a = new FooObject(uniqid('a')); + $b = new BarObject(uniqid('b')); + $c = new FooObject(uniqid('c')); + + $user11Sid = $this->generateSidForUser('user11'); + $user12Sid = $this->generateSidForUser('user12'); + + $this->aclManager + ->addClassPermission($a, 'EDIT', $user11Sid) + ->addClassPermission($b, 'EDIT', $user11Sid) + ->addClassPermission($c, 'VIEW', $user12Sid) + ->addClassPermission($this->barClass, 'VIEW', $user12Sid) + ->addClassPermission($this->fooClass, 'MASTER', 'ROLE_ADMIN'); + + //Revoke all class permission for all sid + $this->aclManager->revokeAllClassPermissions($a); + + $this->authenticateUser('admin', array('ROLE_ADMIN')); + $this->assertFalse($this->aclManager->isGranted('VIEW', $a, 'class')); + $this->assertFalse($this->aclManager->isGranted('VIEW', new ObjectIdentity($this->fooClass, 'class'))); + $this->assertFalse($this->aclManager->isGranted('VIEW', $this->fooClass, 'class')); + + $this->authenticateUser('user11'); + $this->assertFalse($this->aclManager->isGranted('VIEW', $a, 'class')); + $this->assertFalse($this->aclManager->isGranted('VIEW', $c, 'class')); + $this->assertFalse($this->aclManager->isGranted('VIEW', $this->fooClass, 'class')); + $this->assertTrue($this->aclManager->isGranted('EDIT', $b, 'class')); //#6 + + $this->authenticateUser('user12'); + $this->assertFalse($this->aclManager->isGranted('VIEW', $a, 'class')); + $this->assertFalse($this->aclManager->isGranted('VIEW', $c, 'class')); + $this->assertFalse($this->aclManager->isGranted('VIEW', $this->fooClass, 'class')); + + //Revoke all class permission for user12 + $this->aclManager->revokeAllClassPermissions($b, $user12Sid); + + $this->authenticateUser('user11'); + $this->assertTrue($this->aclManager->isGranted('EDIT', $b, 'class')); + + $this->authenticateUser('user12'); + $this->assertFalse($this->aclManager->isGranted('VIEW', new ObjectIdentity($this->barClass, 'class'))); + $this->assertFalse($this->aclManager->isGranted('VIEW', $this->barClass, 'class')); + $this->assertFalse($this->aclManager->isGranted('VIEW', $b, 'class')); + } + + public function testRevokeAllClassFieldPermissions() + { + $a = new FooObject(uniqid('a')); + $b = new BarObject(uniqid('b')); + + $user13Sid = $this->generateSidForUser('user13'); + $user14Sid = $this->generateSidForUser('user14'); + + $this->aclManager + ->addClassPermission($a, 'EDIT', $user13Sid) + ->addClassPermission($b, 'EDIT', $user13Sid) + ->addClassPermission($b, 'EDIT', $user14Sid); + + $this->authenticateUser('user13'); + $this->assertTrue($this->aclManager->isGranted('EDIT', $a, 'class')); + $this->assertTrue($this->aclManager->isGranted('EDIT', $b, 'class')); + + $this->authenticateUser('user14'); + $this->assertTrue($this->aclManager->isGranted('EDIT', $b, 'class')); + + $this->aclManager->revokeAllClassPermissions($a); + + //From now class FooObject loose all class based acl for all sid + //So user13 can't view, edit FooObject, but still do it on BarObject + $this->assertFalse($this->aclManager->isGranted('EDIT', $a, 'class')); + $this->assertTrue($this->aclManager->isGranted('EDIT', $b, 'class')); //Currently that bug + + $this->authenticateUser('user14'); + $this->assertTrue($this->aclManager->isGranted('EDIT', $b, 'class')); //Currently that bug + } + + public function testDeleteAclFor() + { + $a = new FooObject(uniqid('a')); + $b = new BarObject(uniqid('b')); + + $user15Sid = $this->generateSidForUser('user15'); + $user16Sid = $this->generateSidForUser('user16'); + + $this->aclManager + ->addObjectPermission($a, 'EDIT', $user15Sid) + ->addObjectFieldPermission($b, 'securedField', 'EDIT', $user15Sid) + ->addObjectPermission($b, 'EDIT', $user16Sid) + ->addObjectPermission($a, 'MASTER', $user16Sid); + + $this->authenticateUser('user15'); + $this->assertTrue($this->aclManager->isGranted('EDIT', $a)); + $this->assertTrue($this->aclManager->isFieldGranted('EDIT', $b, 'securedField')); + $this->assertFalse($this->aclManager->isGranted('EDIT', $b)); + $this->assertFalse($this->aclManager->isGranted('VIEW', $b)); + + $this->authenticateUser('user16'); + $this->assertTrue($this->aclManager->isGranted('EDIT', $b)); + $this->assertTrue($this->aclManager->isGranted('MASTER', $a)); + + $this->aclManager->deleteAclFor($a, 'object'); //Delete only acl typed as object + + $this->authenticateUser('user15'); + $this->assertFalse($this->aclManager->isGranted('EDIT', $a)); //Deleted acl + $this->assertTrue($this->aclManager->isFieldGranted('EDIT', $b, 'securedField')); //kept + + $this->authenticateUser('user16'); + $this->assertFalse($this->aclManager->isGranted('MASTER', $a)); //Deleted acl + + $this->aclManager->deleteAclFor($b); + + $this->authenticateUser('user15'); + $this->assertFalse($this->aclManager->isFieldGranted('EDIT', $b, 'securedField')); + + $this->authenticateUser('user16'); + $this->assertFalse($this->aclManager->isGranted('EDIT', $b)); + + $this->aclManager + ->addClassFieldPermission($a, 'securedField', 'EDIT', $user15Sid) + ->addClassPermission($b, 'EDIT', $user16Sid) + ->addClassPermission($a, 'VIEW', $user16Sid); + + $this->aclManager->deleteAclFor($a, 'class'); + + $this->authenticateUser('user15'); + $this->assertFalse($this->aclManager->isFieldGranted('EDIT', $a, 'securedField', 'class')); + + $this->authenticateUser('user16'); + $this->assertFalse($this->aclManager->isGranted('VIEW', $a, 'class')); + $this->assertTrue($this->aclManager->isGranted('EDIT', $b, 'class')); + } +} diff --git a/Tests/Model/BarObject.php b/Tests/Model/BarObject.php new file mode 100644 index 0000000..6fffa3a --- /dev/null +++ b/Tests/Model/BarObject.php @@ -0,0 +1,74 @@ +id = $id; + $this->foo = $foo; + $this->bar = $bar; + } + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $foo + */ + public function setFoo($foo) + { + $this->foo = $foo; + } + + /** + * @return mixed + */ + public function getBar() + { + return $this->bar; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } + + /** + * @return mixed + */ + public function getSecuredField() + { + return $this->securedField; + } + + /** + * @param mixed $securedField + */ + public function setSecuredField($securedField) + { + $this->securedField = $securedField; + } + + public function getId() + { + return $this->id; + } +} diff --git a/Tests/Model/FooObject.php b/Tests/Model/FooObject.php new file mode 100644 index 0000000..a2c3434 --- /dev/null +++ b/Tests/Model/FooObject.php @@ -0,0 +1,74 @@ +id = $id; + $this->foo = $foo; + $this->bar = $bar; + } + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $foo + */ + public function setFoo($foo) + { + $this->foo = $foo; + } + + /** + * @return mixed + */ + public function getBar() + { + return $this->bar; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } + + /** + * @return mixed + */ + public function getSecuredField() + { + return $this->securedField; + } + + /** + * @param mixed $securedField + */ + public function setSecuredField($securedField) + { + $this->securedField = $securedField; + } + + public function getId() + { + return $this->id; + } +} diff --git a/Tests/Security/AbstractSecurityTest.php b/Tests/Security/AbstractSecurityTest.php new file mode 100644 index 0000000..cb3127c --- /dev/null +++ b/Tests/Security/AbstractSecurityTest.php @@ -0,0 +1,112 @@ +client = static::createClient(); + $this->container = $this->client->getContainer(); + $this->authenticateUser('user1'); + + $this->connection = $this->container->get('database_connection'); + + if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers())) { + $this->markTestSkipped('This test requires SQLite support in your environment.'); + } + + $this->tableNames = array( + 'oid_table_name' => 'acl_object_identities', + 'oid_ancestors_table_name' => 'acl_object_identity_ancestors', + 'class_table_name' => 'acl_classes', + 'sid_table_name' => 'acl_security_identities', + 'entry_table_name' => 'acl_entries', + ); + + $schema = new Schema($this->tableNames); + + foreach ($schema->toSql($this->connection->getDatabasePlatform()) as $sql) { + $this->connection->exec($sql); + } + + $this->aclManager = $this->container->get('problematic.acl_manager'); + } + + protected function generateSidForUser($username) + { + return new UserSecurityIdentity($username, 'Symfony\Component\Security\Core\User\User'); + } + + protected function resetDB() + { + foreach ($this->tableNames as $table) { + $this->connection->exec(sprintf('TRUNCATE TABLE `%s`', $table)); + } + } + + protected function authenticateUser($username, array $roles = array()) + { + $this->token = $this->createToken($username, $roles); + $this->container->get('security.context')->setToken($this->token); + $this->assertTrue($this->token->isAuthenticated()); + } + + protected function createToken($username, array $roles = array()) + { + $roles = array_merge(array('ROLE_USER'), $roles); + $user = new User($username, '', $roles); + $token = new UsernamePasswordToken($user, '', 'main', $roles); + return $token; + } + + public function testIfContainerExists() + { + $this->assertNotNull($this->client); + $this->assertNotNull($this->container); + } + + public function testIfSecurityContextLoads() + { + $securityContext = $this->container->get('security.context'); + $this->assertNotNull($securityContext->getToken()); + } +} diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php new file mode 100644 index 0000000..a999c2c --- /dev/null +++ b/Tests/bootstrap.php @@ -0,0 +1,2 @@ + + + + + + + + + + ./Tests + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + +