From b4288459cc440fda95eb7f2b431d04dd3e54f80e Mon Sep 17 00:00:00 2001 From: Johannes Schmitt Date: Tue, 14 Dec 2010 16:43:40 +0100 Subject: [PATCH] added ACL system to the Security Component --- .../DoctrineBundle/Resources/config/orm.xml | 3 + .../Security/AclCollectionCache.php | 58 ++ .../Command/InitAclCommand.php | 67 ++ .../DependencyInjection/SecurityExtension.php | 20 + .../Resources/config/security_acl.xml | 76 ++ .../Templating/Helper/SecurityHelper.php | 11 +- .../Extension/SecurityExtension.php | 10 +- .../Security/Acl/Dbal/AclProvider.php | 624 ++++++++++++ .../Security/Acl/Dbal/MutableAclProvider.php | 887 ++++++++++++++++++ .../Component/Security/Acl/Dbal/Schema.php | 145 +++ .../Component/Security/Acl/Domain/Acl.php | 679 ++++++++++++++ .../Security/Acl/Domain/AuditLogger.php | 53 ++ .../Security/Acl/Domain/DoctrineAclCache.php | 222 +++++ .../Component/Security/Acl/Domain/Entry.php | 215 +++++ .../Security/Acl/Domain/FieldEntry.php | 88 ++ .../Security/Acl/Domain/ObjectIdentity.php | 106 +++ .../ObjectIdentityRetrievalStrategy.php | 35 + .../Acl/Domain/PermissionGrantingStrategy.php | 229 +++++ .../Acl/Domain/RoleSecurityIdentity.php | 74 ++ .../SecurityIdentityRetrievalStrategy.php | 73 ++ .../Acl/Domain/UserSecurityIdentity.php | 83 ++ .../Exception/AclAlreadyExistsException.php | 22 + .../Acl/Exception/AclNotFoundException.php | 22 + .../ConcurrentModificationException.php | 13 + .../Security/Acl/Exception/Exception.php | 21 + .../InvalidDomainObjectException.php | 13 + .../Acl/Exception/NoAceFoundException.php | 22 + .../Acl/Exception/SidNotLoadedException.php | 22 + .../Security/Acl/Model/AclCacheInterface.php | 69 ++ .../Security/Acl/Model/AclInterface.php | 106 +++ .../Acl/Model/AclProviderInterface.php | 49 + .../Acl/Model/AuditLoggerInterface.php | 30 + .../Acl/Model/AuditableAclInterface.php | 63 ++ .../Acl/Model/AuditableEntryInterface.php | 34 + .../Acl/Model/DomainObjectInterface.php | 29 + .../Security/Acl/Model/EntryInterface.php | 65 ++ .../Acl/Model/FieldAwareEntryInterface.php | 22 + .../Acl/Model/MutableAclInterface.php | 174 ++++ .../Acl/Model/MutableAclProviderInterface.php | 52 + .../Acl/Model/ObjectIdentityInterface.php | 49 + ...jectIdentityRetrievalStrategyInterface.php | 19 + .../PermissionGrantingStrategyInterface.php | 43 + .../Acl/Model/SecurityIdentityInterface.php | 31 + ...rityIdentityRetrievalStrategyInterface.php | 25 + .../Acl/Permission/BasicPermissionMap.php | 103 ++ .../Security/Acl/Permission/MaskBuilder.php | 202 ++++ .../Acl/Permission/PermissionMapInterface.php | 39 + .../Component/Security/Acl/Voter/AclVoter.php | 105 +++ .../Security/Acl/Voter/FieldVote.php | 40 + .../Acl/Dbal/AclProviderBenchmarkTest.php | 258 +++++ .../Security/Acl/Dbal/AclProviderTest.php | 242 +++++ .../Acl/Dbal/MutableAclProviderTest.php | 452 +++++++++ .../Component/Security/Acl/Domain/AclTest.php | 502 ++++++++++ .../Security/Acl/Domain/AuditLoggerTest.php | 76 ++ .../Acl/Domain/DoctrineAclCacheTest.php | 94 ++ .../Security/Acl/Domain/EntryTest.php | 110 +++ .../Security/Acl/Domain/FieldEntryTest.php | 65 ++ .../ObjectIdentityRetrievalStrategyTest.php | 34 + .../Acl/Domain/ObjectIdentityTest.php | 76 ++ .../Domain/PermissionGrantingStrategyTest.php | 190 ++++ .../Acl/Domain/RoleSecurityIdentityTest.php | 49 + .../SecurityIdentityRetrievalStrategyTest.php | 131 +++ .../Acl/Domain/UserSecurityIdentityTest.php | 61 ++ .../Acl/Permission/MaskBuilderTest.php | 94 ++ 64 files changed, 7674 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Bundle/DoctrineBundle/Security/AclCollectionCache.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Command/InitAclCommand.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/security_acl.xml create mode 100644 src/Symfony/Component/Security/Acl/Dbal/AclProvider.php create mode 100644 src/Symfony/Component/Security/Acl/Dbal/MutableAclProvider.php create mode 100644 src/Symfony/Component/Security/Acl/Dbal/Schema.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/Acl.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/AuditLogger.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/DoctrineAclCache.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/Entry.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/FieldEntry.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/ObjectIdentity.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/ObjectIdentityRetrievalStrategy.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/PermissionGrantingStrategy.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/RoleSecurityIdentity.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/SecurityIdentityRetrievalStrategy.php create mode 100644 src/Symfony/Component/Security/Acl/Domain/UserSecurityIdentity.php create mode 100644 src/Symfony/Component/Security/Acl/Exception/AclAlreadyExistsException.php create mode 100644 src/Symfony/Component/Security/Acl/Exception/AclNotFoundException.php create mode 100644 src/Symfony/Component/Security/Acl/Exception/ConcurrentModificationException.php create mode 100644 src/Symfony/Component/Security/Acl/Exception/Exception.php create mode 100644 src/Symfony/Component/Security/Acl/Exception/InvalidDomainObjectException.php create mode 100644 src/Symfony/Component/Security/Acl/Exception/NoAceFoundException.php create mode 100644 src/Symfony/Component/Security/Acl/Exception/SidNotLoadedException.php create mode 100644 src/Symfony/Component/Security/Acl/Model/AclCacheInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/AclInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/AclProviderInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/AuditLoggerInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/AuditableAclInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/AuditableEntryInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/DomainObjectInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/EntryInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/FieldAwareEntryInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/MutableAclInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/MutableAclProviderInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/ObjectIdentityInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/ObjectIdentityRetrievalStrategyInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/PermissionGrantingStrategyInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/SecurityIdentityInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Model/SecurityIdentityRetrievalStrategyInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Permission/BasicPermissionMap.php create mode 100644 src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php create mode 100644 src/Symfony/Component/Security/Acl/Permission/PermissionMapInterface.php create mode 100644 src/Symfony/Component/Security/Acl/Voter/AclVoter.php create mode 100644 src/Symfony/Component/Security/Acl/Voter/FieldVote.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Dbal/AclProviderBenchmarkTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Dbal/AclProviderTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Dbal/MutableAclProviderTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/AclTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/AuditLoggerTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/DoctrineAclCacheTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/EntryTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/FieldEntryTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/ObjectIdentityRetrievalStrategyTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/ObjectIdentityTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/PermissionGrantingStrategyTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/RoleSecurityIdentityTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/SecurityIdentityRetrievalStrategyTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Domain/UserSecurityIdentityTest.php create mode 100644 tests/Symfony/Tests/Component/Security/Acl/Permission/MaskBuilderTest.php diff --git a/src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml b/src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml index 78a3bc20c7b6..18e393332e6a 100644 --- a/src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml +++ b/src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml @@ -38,6 +38,9 @@ Symfony\Bundle\DoctrineBundle\Security\EntityUserProvider + + + Symfony\Bundle\DoctrineBundle\Security\AclCollectionCache diff --git a/src/Symfony/Bundle/DoctrineBundle/Security/AclCollectionCache.php b/src/Symfony/Bundle/DoctrineBundle/Security/AclCollectionCache.php new file mode 100644 index 000000000000..e1448a197438 --- /dev/null +++ b/src/Symfony/Bundle/DoctrineBundle/Security/AclCollectionCache.php @@ -0,0 +1,58 @@ + + */ +class AclCollectionCache +{ + protected $aclProvider; + protected $objectIdentityRetrievalStrategy; + protected $securityIdentityRetrievalStrategy; + + /** + * Constructor + * + * @param AclProviderInterface $aclProvider + * @param ObjectIdentityRetrievalStrategy $oidRetrievalStrategy + * @param SecurityIdentityRetrievalStrategy $sidRetrievalStrategy + * @return void + */ + public function __construct(AclProviderInterface $aclProvider, ObjectIdentityRetrievalStrategyInterface $oidRetrievalStrategy, SecurityIdentityRetrievalStrategyInterface $sidRetrievalStrategy) + { + $this->aclProvider = $aclProvider; + $this->objectIdentityRetrievalStrategy = $oidRetrievalStrategy; + $this->securityIdentityRetrievalStrategy = $sidRetrievalStrategy; + } + + /** + * Batch loads ACLs for an entire collection; thus, it reduces the number + * of required queries considerably. + * + * @param Collection $collection + * @param array $tokens an array of TokenInterface implementations + * @return void + */ + public function cache(Collection $collection, array $tokens = array()) + { + $sids = array(); + foreach ($tokens as $token) { + $sids = array_merge($sids, $this->securityIdentityRetrievalStrategy->getSecurityIdentities($token)); + } + + $oids = array(); + foreach ($collection as $domainObject) { + $oids[] = $this->objectIdentityRetrievalStrategy->getObjectIdentity($domainObject); + } + + $this->aclProvider->findAcls($oids, $sids); + } +} \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/InitAclCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/InitAclCommand.php new file mode 100644 index 000000000000..3bcab49e6ae4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/InitAclCommand.php @@ -0,0 +1,67 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Installs the tables required by the ACL system + * + * @author Johannes M. Schmitt + */ +class InitAclCommand extends Command +{ + /** + * @see Command + */ + protected function configure() + { + $this + ->setName('init:acl') + ; + } + + /** + * @see Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $connection = $this->container->get('security.acl.dbal.connection'); + $sm = $connection->getSchemaManager(); + $tableNames = $sm->listTableNames(); + $tables = array( + 'class_table_name' => $this->container->getParameter('security.acl.dbal.class_table_name'), + 'sid_table_name' => $this->container->getParameter('security.acl.dbal.sid_table_name'), + 'oid_table_name' => $this->container->getParameter('security.acl.dbal.oid_table_name'), + 'oid_ancestors_table_name' => $this->container->getParameter('security.acl.dbal.oid_ancestors_table_name'), + 'entry_table_name' => $this->container->getParameter('security.acl.dbal.entry_table_name'), + ); + + foreach ($tables as $table) { + if (in_array($table, $tableNames, true)) { + $output->writeln(sprintf('The table "%s" already exists. Aborting.', $table)); + return; + } + } + + $schema = new Schema($tables); + foreach ($schema->toSql($connection->getDatabasePlatform()) as $sql) { + $connection->exec($sql); + } + + $output->writeln('ACL tables have been initialized successfully.'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php index 930cd4d927fc..2f72b55542c1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php @@ -2,6 +2,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; +use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Resource\FileResource; @@ -487,6 +488,25 @@ protected function createSwitchUserListener($container, $id, $config, $defaultPr return $switchUserListenerId; } + + public function aclLoad(array $config, ContainerBuilder $container) + { + if (!$container->hasDefinition('security.acl')) { + $loader = new XmlFileLoader($container, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config')); + $loader->load('security_acl.xml'); + } + + if (isset($config['connection'])) { + $container->setAlias(sprintf('doctrine.dbal.%s_connection', $config['connection']), 'security.acl.dbal.connection'); + } + + if (isset($config['cache'])) { + $container->setAlias('security.acl.cache', sprintf('security.acl.cache.%s', $config['cache'])); + } else { + $container->remove('security.acl.cache.doctrine'); + $container->removeAlias('security.acl.cache.doctrine.cache_impl'); + } + } /** * Returns the base path for the XSD files. diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_acl.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_acl.xml new file mode 100644 index 000000000000..ddb26295c660 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_acl.xml @@ -0,0 +1,76 @@ + + + + + + acl_classes + acl_entries + acl_object_identities + acl_object_identity_ancestors + acl_security_identities + Symfony\Component\Security\Acl\Dbal\MutableAclProvider + + Symfony\Component\Security\Acl\Domain\PermissionGrantingStrategy + + Symfony\Component\Security\Acl\Voter\AclVoter + Symfony\Component\Security\Acl\Permission\BasicPermissionMap + + Symfony\Component\Security\Acl\Domain\ObjectIdentityRetrievalStrategy + Symfony\Component\Security\Acl\Domain\SecurityIdentityRetrievalStrategy + + Symfony\Component\Security\Acl\Domain\DoctrineAclCache + sf2_acl_ + + + + + + + + + + + + + + + + + %security.acl.dbal.class_table_name% + %security.acl.dbal.entry_table_name% + %security.acl.dbal.oid_table_name% + %security.acl.dbal.oid_ancestors_table_name% + %security.acl.dbal.sid_table_name% + + + + + + + + + + + + + + + + %security.acl.cache.doctrine.prefix% + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SecurityHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SecurityHelper.php index 6f6180600347..527809b68e9e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SecurityHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SecurityHelper.php @@ -2,6 +2,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; +use Symfony\Component\Security\Acl\Voter\FieldVote; use Symfony\Component\Templating\Helper\Helper; use Symfony\Component\Security\SecurityContext; @@ -33,11 +34,19 @@ public function __construct(SecurityContext $context = null) $this->context = $context; } - public function vote($role, $object = null) + public function vote($role, $object = null, $field = null) { if (null === $this->context) { return false; } + + if ($field !== null) { + if (null === $object) { + throw new \InvalidArgumentException('$object cannot be null when field is not null.'); + } + + $object = new FieldVote($object, $field); + } return $this->context->vote($role, $object); } diff --git a/src/Symfony/Bundle/TwigBundle/Extension/SecurityExtension.php b/src/Symfony/Bundle/TwigBundle/Extension/SecurityExtension.php index 0b0b2d940980..970ea59d487e 100644 --- a/src/Symfony/Bundle/TwigBundle/Extension/SecurityExtension.php +++ b/src/Symfony/Bundle/TwigBundle/Extension/SecurityExtension.php @@ -27,11 +27,19 @@ public function __construct(SecurityContext $context = null) $this->context = $context; } - public function vote($role, $object = null) + public function vote($role, $object = null, $field = null) { if (null === $this->context) { return false; } + + if ($field !== null) { + if (null === $object) { + throw new \InvalidArgumentException('$object cannot be null when field is not null.'); + } + + $object = new FieldVote($object, $field); + } return $this->context->vote($role, $object); } diff --git a/src/Symfony/Component/Security/Acl/Dbal/AclProvider.php b/src/Symfony/Component/Security/Acl/Dbal/AclProvider.php new file mode 100644 index 000000000000..3664b0c52cc9 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Dbal/AclProvider.php @@ -0,0 +1,624 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * An ACL provider implementation. + * + * This provider assumes that all ACLs share the same PermissionGrantingStrategy. + * + * @author Johannes M. Schmitt + */ +class AclProvider implements AclProviderInterface +{ + const MAX_BATCH_SIZE = 30; + + protected $aclCache; + protected $connection; + protected $loadedAces; + protected $loadedAcls; + protected $options; + protected $permissionGrantingStrategy; + + /** + * Constructor + * + * @param Connection $connection + * @param PermissionGrantingStrategyInterface $permissionGrantingStrategy + * @param array $options + * @param AclCacheInterface $aclCache + */ + public function __construct(Connection $connection, PermissionGrantingStrategyInterface $permissionGrantingStrategy, array $options, AclCacheInterface $aclCache = null) + { + $this->aclCache = $aclCache; + $this->connection = $connection; + $this->loadedAces = array(); + $this->loadedAcls = array(); + $this->options = $options; + $this->permissionGrantingStrategy = $permissionGrantingStrategy; + } + + /** + * {@inheritDoc} + */ + public function findChildren(ObjectIdentityInterface $parentOid, $directChildrenOnly = false) + { + $sql = $this->getFindChildrenSql($parentOid, $directChildrenOnly); + + $children = array(); + foreach ($this->connection->executeQuery($sql)->fetchAll() as $data) { + $children[] = new ObjectIdentity($data['object_identifier'], $data['class_type']); + } + + return $children; + } + + /** + * {@inheritDoc} + */ + public function findAcl(ObjectIdentityInterface $oid, array $sids = array()) + { + return $this->findAcls(array($oid), $sids)->offsetGet($oid); + } + + /** + * {@inheritDoc} + */ + public function findAcls(array $oids, array $sids = array()) + { + $result = new \SplObjectStorage(); + $currentBatch = array(); + $oidLookup = array(); + + for ($i=0,$c=count($oids); $i<$c; $i++) { + $oid = $oids[$i]; + $oidLookupKey = $oid->getIdentifier().$oid->getType(); + $oidLookup[$oidLookupKey] = $oid; + $aclFound = false; + + // check if result already contains an ACL + if ($result->contains($oid)) { + $aclFound = true; + } + + // check if this ACL has already been hydrated + if (!$aclFound && isset($this->loadedAcls[$oid->getType()][$oid->getIdentifier()])) { + $acl = $this->loadedAcls[$oid->getType()][$oid->getIdentifier()]; + + if (!$acl->isSidLoaded($sids)) { + // FIXME: we need to load ACEs for the missing SIDs. This is never + // reached by the default implementation, since we do not + // filter by SID + throw new \RuntimeException('This is not supported by the default implementation.'); + } else { + $result->attach($oid, $acl); + $aclFound = true; + } + } + + // check if we can locate the ACL in the cache + if (!$aclFound && null !== $this->aclCache) { + $acl = $this->aclCache->getFromCacheByIdentity($oid); + + if (null !== $acl) { + if ($acl->isSidLoaded($sids)) { + // check if any of the parents has been loaded since we need to + // ensure that there is only ever one ACL per object identity + $parentAcl = $acl->getParentAcl(); + while (null !== $parentAcl) { + $parentOid = $parentAcl->getObjectIdentity(); + + if (isset($this->loadedAcls[$parentOid->getType()][$parentOid->getIdentifier()])) { + $acl->setParentAcl($this->loadedAcls[$parentOid->getType()][$parentOid->getIdentifier()]); + break; + } else { + $this->loadedAcls[$parentOid->getType()][$parentOid->getIdentifier()] = $parentAcl; + $this->updateAceIdentityMap($parentAcl); + } + + $parentAcl = $parentAcl->getParentAcl(); + } + + $this->loadedAcls[$oid->getType()][$oid->getIdentifier()] = $acl; + $this->updateAceIdentityMap($acl); + $result->attach($oid, $acl); + $aclFound = true; + } else { + $this->aclCache->evictFromCacheByIdentity($oid); + + foreach ($this->findChildren($oid) as $childOid) { + $this->aclCache->evictFromCacheByIdentity($childOid); + } + } + } + } + + // looks like we have to load the ACL from the database + if (!$aclFound) { + $currentBatch[] = $oid; + } + + // Is it time to load the current batch? + if ((self::MAX_BATCH_SIZE === count($currentBatch) || ($i + 1) === $c) && count($currentBatch) > 0) { + $loadedBatch = $this->lookupObjectIdentities($currentBatch, $sids, $oidLookup); + + foreach ($loadedBatch as $loadedOid) { + $loadedAcl = $loadedBatch->offsetGet($loadedOid); + + if (null !== $this->aclCache) { + $this->aclCache->putInCache($loadedAcl); + } + + if (isset($oidLookup[$loadedOid->getIdentifier().$loadedOid->getType()])) { + $result->attach($loadedOid, $loadedAcl); + } + } + + $currentBatch = array(); + } + } + + // check that we got ACLs for all the identities + foreach ($oids as $oid) { + if (!$result->contains($oid)) { + throw new AclNotFoundException(sprintf('No ACL found for %s.', $oid)); + } + } + + return $result; + } + + /** + * This method is called when an ACL instance is retrieved from the cache. + * + * @param AclInterface $acl + * @return void + */ + protected function updateAceIdentityMap(AclInterface $acl) + { + foreach (array('classAces', 'classFieldAces', 'objectAces', 'objectFieldAces') as $property) { + $reflection = new \ReflectionProperty($acl, $property); + $reflection->setAccessible(true); + $value = $reflection->getValue($acl); + + if ('classAces' === $property || 'objectAces' === $property) { + $this->doUpdateAceIdentityMap($value); + } else { + foreach ($value as $field => $aces) { + $this->doUpdateAceIdentityMap($value[$field]); + } + } + + $reflection->setValue($acl, $value); + $reflection->setAccessible(false); + } + } + + /** + * Does either overwrite the passed ACE, or saves it in the global identity + * map to ensure every ACE only gets instantiated once. + * + * @param array $aces + * @return void + */ + protected function doUpdateAceIdentityMap(array &$aces) + { + foreach ($aces as $index => $ace) { + if (isset($this->loadedAces[$ace->getId()])) { + $aces[$index] = $this->loadedAces[$ace->getId()]; + } else { + $this->loadedAces[$ace->getId()] = $ace; + } + } + } + + /** + * This method is called for object identities which could not be retrieved + * from the cache, and for which thus a database query is required. + * + * @param array $batch + * @param array $sids + * @param array $oidLookup + * @return \SplObjectStorage mapping object identites to ACL instances + */ + protected function lookupObjectIdentities(array $batch, array $sids, array $oidLookup) + { + $sql = $this->getLookupSql($batch, $sids); + $stmt = $this->connection->executeQuery($sql); + + return $this->hydrateObjectIdentities($stmt, $oidLookup, $sids); + } + + /** + * This method is called to hydrate ACLs and ACEs. + * + * This method was designed for performance; thus, a lot of code has been + * inlined at the cost of readability, and maintainability. + * + * Keep in mind that changes to this method might severely reduce the + * performance of the entire ACL system. + * + * @param Statement $stmt + * @param array $oidLookup + * @param array $sids + * @throws \RuntimeException + * @return \SplObjectStorage + */ + protected function hydrateObjectIdentities(Statement $stmt, array $oidLookup, array $sids) { + $parentIdToFill = new \SplObjectStorage(); + $acls = $aces = $emptyArray = array(); + $oidCache = $oidLookup; + $result = new \SplObjectStorage(); + $loadedAces =& $this->loadedAces; + $loadedAcls =& $this->loadedAcls; + $permissionGrantingStrategy = $this->permissionGrantingStrategy; + + // we need these to set protected properties on hydrated objects + $aclReflection = new \ReflectionClass('Symfony\Component\Security\Acl\Domain\Acl'); + $aclClassAcesProperty = $aclReflection->getProperty('classAces'); + $aclClassAcesProperty->setAccessible(true); + $aclClassFieldAcesProperty = $aclReflection->getProperty('classFieldAces'); + $aclClassFieldAcesProperty->setAccessible(true); + $aclObjectAcesProperty = $aclReflection->getProperty('objectAces'); + $aclObjectAcesProperty->setAccessible(true); + $aclObjectFieldAcesProperty = $aclReflection->getProperty('objectFieldAces'); + $aclObjectFieldAcesProperty->setAccessible(true); + $aclParentAclProperty = $aclReflection->getProperty('parentAcl'); + $aclParentAclProperty->setAccessible(true); + + // fetchAll() consumes more memory than consecutive calls to fetch(), + // but it is faster + foreach ($stmt->fetchAll(\PDO::FETCH_NUM) as $data) { + list($aclId, + $objectIdentifier, + $parentObjectIdentityId, + $entriesInheriting, + $classType, + $aceId, + $objectIdentityId, + $fieldName, + $aceOrder, + $mask, + $granting, + $grantingStrategy, + $auditSuccess, + $auditFailure, + $username, + $securityIdentifier) = $data; + + // has the ACL been hydrated during this hydration cycle? + if (isset($acls[$aclId])) { + $acl = $acls[$aclId]; + } + + // has the ACL been hydrated during any previous cycle, or was possibly loaded + // from cache? + else if (isset($loadedAcls[$classType][$objectIdentifier])) { + $acl = $loadedAcls[$classType][$objectIdentifier]; + + // keep reference in local array (saves us some hash calculations) + $acls[$aclId] = $acl; + + // attach ACL to the result set; even though we do not enforce that every + // object identity has only one instance, we must make sure to maintain + // referential equality with the oids passed to findAcls() + if (!isset($oidCache[$objectIdentifier.$classType])) { + $oidCache[$objectIdentifier.$classType] = $acl->getObjectIdentity(); + } + $result->attach($oidCache[$objectIdentifier.$classType], $acl); + } + + // so, this hasn't been hydrated yet + else { + // create object identity if we haven't done so yet + $oidLookupKey = $objectIdentifier.$classType; + if (!isset($oidCache[$oidLookupKey])) { + $oidCache[$oidLookupKey] = new ObjectIdentity($objectIdentifier, $classType); + } + + $acl = new Acl((integer) $aclId, $oidCache[$oidLookupKey], $permissionGrantingStrategy, $emptyArray, !!$entriesInheriting); + + // keep a local, and global reference to this ACL + $loadedAcls[$classType][$objectIdentifier] = $acl; + $acls[$aclId] = $acl; + + // try to fill in parent ACL, or defer until all ACLs have been hydrated + if (null !== $parentObjectIdentityId) { + if (isset($acls[$parentObjectIdentityId])) { + $aclParentAclProperty->setValue($acl, $acls[$parentObjectIdentityId]); + } else { + $parentIdToFill->attach($acl, $parentObjectIdentityId); + } + } + + $result->attach($oidCache[$oidLookupKey], $acl); + } + + // check if this row contains an ACE record + if (null !== $aceId) { + // have we already hydrated ACEs for this ACL? + if (!isset($aces[$aclId])) { + $aces[$aclId] = array($emptyArray, $emptyArray, $emptyArray, $emptyArray); + } + + // has this ACE already been hydrated during a previous cycle, or + // possible been loaded from cache? + // It is important to only ever have one ACE instance per actual row since + // some ACEs are shared between ACL instances + if (!isset($loadedAces[$aceId])) { + if (!isset($sids[$key = ($username?'1':'0').$securityIdentifier])) { + if ($username) { + $sids[$key] = new UserSecurityIdentity($securityIdentifier); + } else { + $sids[$key] = new RoleSecurityIdentity($securityIdentifier); + } + } + + if (null === $fieldName) { + $loadedAces[$aceId] = new Entry((integer) $aceId, $acl, $sids[$key], $grantingStrategy, (integer) $mask, !!$granting, !!$auditFailure, !!$auditSuccess); + } else { + $loadedAces[$aceId] = new FieldEntry((integer) $aceId, $acl, $fieldName, $sids[$key], $grantingStrategy, (integer) $mask, !!$granting, !!$auditFailure, !!$auditSuccess); + } + } + $ace = $loadedAces[$aceId]; + + // assign ACE to the correct property + if (null === $objectIdentityId) { + if (null === $fieldName) { + $aces[$aclId][0][$aceOrder] = $ace; + } else { + $aces[$aclId][1][$fieldName][$aceOrder] = $ace; + } + } else { + if (null === $fieldName) { + $aces[$aclId][2][$aceOrder] = $ace; + } else { + $aces[$aclId][3][$fieldName][$aceOrder] = $ace; + } + } + } + } + + // We do not sort on database level since we only want certain subsets to be sorted, + // and we are going to read the entire result set anyway. + // Sorting on DB level increases query time by an order of magnitude while it is + // almost negligible when we use PHPs array sort functions. + foreach ($aces as $aclId => $aceData) { + $acl = $acls[$aclId]; + + ksort($aceData[0]); + $aclClassAcesProperty->setValue($acl, $aceData[0]); + + foreach (array_keys($aceData[1]) as $fieldName) { + ksort($aceData[1][$fieldName]); + } + $aclClassFieldAcesProperty->setValue($acl, $aceData[1]); + + ksort($aceData[2]); + $aclObjectAcesProperty->setValue($acl, $aceData[2]); + + foreach (array_keys($aceData[3]) as $fieldName) { + ksort($aceData[3][$fieldName]); + } + $aclObjectFieldAcesProperty->setValue($acl, $aceData[3]); + } + + // fill-in parent ACLs where this hasn't been done yet cause the parent ACL was not + // yet available + $processed = 0; + foreach ($parentIdToFill as $acl) + { + $parentId = $parentIdToFill->offsetGet($acl); + + // let's see if we have already hydrated this + if (isset($acls[$parentId])) { + $aclParentAclProperty->setValue($acl, $acls[$parentId]); + $processed += 1; + + continue; + } + } + + // reset reflection changes + $aclClassAcesProperty->setAccessible(false); + $aclClassFieldAcesProperty->setAccessible(false); + $aclObjectAcesProperty->setAccessible(false); + $aclObjectFieldAcesProperty->setAccessible(false); + $aclParentAclProperty->setAccessible(false); + + // this should never be true if the database integrity hasn't been compromised + if ($processed < count($parentIdToFill)) { + throw new \RuntimeException('Not all parent ids were populated. This implies an integrity problem.'); + } + + return $result; + } + + /** + * Constructs the query used for looking up object identites and associated + * ACEs, and security identities. + * + * @param array $batch + * @param array $sids + * @throws AclNotFoundException + * @return string + */ + protected function getLookupSql(array $batch, array $sids) + { + // FIXME: add support for filtering by sids (right now we select all sids) + + $ancestorIds = $this->getAncestorIds($batch); + if (0 === count($ancestorIds)) { + throw new AclNotFoundException('There is no ACL for the given object identity.'); + } + + $sql = <<options['oid_table_name']} o + INNER JOIN {$this->options['class_table_name']} c ON c.id = o.class_id + LEFT JOIN {$this->options['entry_table_name']} e ON ( + e.class_id = o.class_id AND (e.object_identity_id = o.id OR {$this->connection->getDatabasePlatform()->getIsNullExpression('e.object_identity_id')}) + ) + LEFT JOIN {$this->options['sid_table_name']} s ON ( + s.id = e.security_identity_id + ) + + WHERE (o.id = +SELECTCLAUSE; + + $sql .= implode(' OR o.id = ', $ancestorIds).')'; + + return $sql; + } + + /** + * Retrieves all the ids which need to be queried from the database + * including the ids of parent ACLs. + * + * @param array $batch + * @return array + */ + protected function getAncestorIds(array &$batch) + { + $sql = <<connection->quote($batch[$i]->getIdentifier()), + $this->connection->quote($batch[$i]->getType()) + ); + + if ($i+1 < $c) { + $sql .= ' OR '; + } + } + + $sql .= ')'; + + $ancestorIds = array(); + foreach ($this->connection->executeQuery($sql)->fetchAll() as $data) { + // FIXME: skip ancestors which are cached + + $ancestorIds[] = $data['ancestor_id']; + } + + return $ancestorIds; + } + + /** + * Constructs the SQL for retrieving child object identities for the given + * object identities. + * + * @param ObjectIdentityInterface $oid + * @param Boolean $directChildrenOnly + * @return string + */ + protected function getFindChildrenSql(ObjectIdentityInterface $oid, $directChildrenOnly) + { + if (false === $directChildrenOnly) { + $query = <<options['oid_table_name']} as o + INNER JOIN {$this->options['class_table_name']} as c ON c.id = o.class_id + INNER JOIN {$this->options['oid_ancestors_table_name']} as a ON a.object_identity_id = o.id + WHERE + a.ancestor_id = %d AND a.object_identity_id != a.ancestor_id +FINDCHILDREN; + } else { + $query = <<options['oid_table_name']} as o + INNER JOIN {$this->options['class_table_name']} as c ON c.id = o.class_id + WHERE o.parent_object_identity_id = %d +FINDCHILDREN; + } + + return sprintf($query, $this->retrieveObjectIdentityPrimaryKey($oid)); + } + + /** + * Constructs the SQL for retrieving the primary key of the given object + * identity. + * + * @param ObjectIdentityInterface $oid + * @return string + */ + protected function getSelectObjectIdentityIdSql(ObjectIdentityInterface $oid) + { + $query = <<options['oid_table_name'], + $this->options['class_table_name'], + $this->connection->quote($oid->getIdentifier()), + $this->connection->quote($oid->getType()) + ); + } + + /** + * Returns the primary key of the passed object identity. + * + * @param ObjectIdentityInterface $oid + * @return integer + */ + protected function retrieveObjectIdentityPrimaryKey(ObjectIdentityInterface $oid) + { + return $this->connection->executeQuery($this->getSelectObjectIdentityIdSql($oid))->fetchColumn(); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Dbal/MutableAclProvider.php b/src/Symfony/Component/Security/Acl/Dbal/MutableAclProvider.php new file mode 100644 index 000000000000..6da3ec8bb169 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Dbal/MutableAclProvider.php @@ -0,0 +1,887 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * An implementation of the MutableAclProviderInterface using Doctrine DBAL. + * + * @author Johannes M. Schmitt + */ +class MutableAclProvider extends AclProvider implements MutableAclProviderInterface, PropertyChangedListener +{ + protected $propertyChanges; + + /** + * {@inheritDoc} + */ + public function __construct(Connection $connection, PermissionGrantingStrategyInterface $permissionGrantingStrategy, array $options, AclCacheInterface $aclCache = null) + { + parent::__construct($connection, $permissionGrantingStrategy, $options, $aclCache); + + $this->propertyChanges = new \SplObjectStorage(); + } + + /** + * {@inheritDoc} + */ + public function createAcl(ObjectIdentityInterface $oid) + { + if (false !== $this->retrieveObjectIdentityPrimaryKey($oid)) { + throw new AclAlreadyExistsException(sprintf('%s is already associated with an ACL.', $oid)); + } + + $this->connection->beginTransaction(); + try { + $this->createObjectIdentity($oid); + + $pk = $this->retrieveObjectIdentityPrimaryKey($oid); + $this->connection->executeQuery($this->getInsertObjectIdentityRelationSql($pk, $pk)); + + $this->connection->commit(); + } catch (\Exception $failed) { + $this->connection->rollBack(); + + throw $failed; + } + + // re-read the ACL from the database to ensure proper caching, etc. + return $this->findAcl($oid); + } + + /** + * {@inheritDoc} + */ + public function deleteAcl(ObjectIdentityInterface $oid) + { + $this->connection->beginTransaction(); + try { + foreach ($this->findChildren($oid, true) as $childOid) { + $this->deleteAcl($childOid); + } + + $oidPK = $this->retrieveObjectIdentityPrimaryKey($oid); + + $this->deleteAccessControlEntries($oidPK); + $this->deleteObjectIdentityRelations($oidPK); + $this->deleteObjectIdentity($oidPK); + + $this->connection->commit(); + } catch (\Exception $failed) { + $this->connection->rollBack(); + + throw $failed; + } + + // evict the ACL from the in-memory identity map + if (isset($this->loadedAcls[$oid->getType()][$oid->getIdentifier()])) { + $this->propertyChanges->offsetUnset($this->loadedAcls[$oid->getType()][$oid->getIdentifier()]); + unset($this->loadedAcls[$oid->getType()][$oid->getIdentifier()]); + } + + // evict the ACL from any caches + if (null !== $this->aclCache) { + $this->aclCache->evictFromCacheByIdentity($oid); + } + } + + /** + * {@inheritDoc} + */ + public function findAcls(array $oids, array $sids = array()) + { + $result = parent::findAcls($oids, $sids); + + foreach ($result as $oid) { + $acl = $result->offsetGet($oid); + + if (false === $this->propertyChanges->contains($acl) && $acl instanceof MutableAclInterface) { + $acl->addPropertyChangedListener($this); + $this->propertyChanges->attach($acl, array()); + } + + $parentAcl = $acl->getParentAcl(); + while (null !== $parentAcl) { + if (false === $this->propertyChanges->contains($parentAcl) && $acl instanceof MutableAclInterface) { + $parentAcl->addPropertyChangedListener($this); + $this->propertyChanges->attach($parentAcl, array()); + } + + $parentAcl = $parentAcl->getParentAcl(); + } + } + + return $result; + } + + /** + * Implementation of PropertyChangedListener + * + * This allows us to keep track of which values have been changed, so we don't + * have to do a full introspection when ->updateAcl() is called. + * + * @param mixed $sender + * @param string $propertyName + * @param mixed $oldValue + * @param mixed $newValue + * @return void + */ + public function propertyChanged($sender, $propertyName, $oldValue, $newValue) + { + if (!$sender instanceof MutableAclInterface && !$sender instanceof EntryInterface) { + throw new \InvalidArgumentException('$sender must be an instance of MutableAclInterface, or EntryInterface.'); + } + + if ($sender instanceof EntryInterface) { + if (null === $sender->getId()) { + return; + } + + $ace = $sender; + $sender = $ace->getAcl(); + } else { + $ace = null; + } + + if (false === $this->propertyChanges->contains($sender)) { + throw new \InvalidArgumentException('$sender is not being tracked by this provider.'); + } + + $propertyChanges = $this->propertyChanges->offsetGet($sender); + if (null === $ace) { + if (isset($propertyChanges[$propertyName])) { + $oldValue = $propertyChanges[$propertyName][0]; + if ($oldValue === $newValue) { + unset($propertyChanges[$propertyName]); + } else { + $propertyChanges[$propertyName] = array($oldValue, $newValue); + } + } else { + $propertyChanges[$propertyName] = array($oldValue, $newValue); + } + } else { + if (!isset($propertyChanges['aces'])) { + $propertyChanges['aces'] = new \SplObjectStorage(); + } + + $acePropertyChanges = $propertyChanges['aces']->contains($ace)? $propertyChanges['aces']->offsetGet($ace) : array(); + + if (isset($acePropertyChanges[$propertyName])) { + $oldValue = $acePropertyChanges[$propertyName][0]; + if ($oldValue === $newValue) { + unset($acePropertyChanges[$propertyName]); + } else { + $acePropertyChanges[$propertyName] = array($oldValue, $newValue); + } + } else { + $acePropertyChanges[$propertyName] = array($oldValue, $newValue); + } + + if (count($acePropertyChanges) > 0) { + $propertyChanges['aces']->offsetSet($ace, $acePropertyChanges); + } else { + $propertyChanges['aces']->offsetUnset($ace); + + if (0 === count($propertyChanges['aces'])) { + unset($propertyChanges['aces']); + } + } + } + + $this->propertyChanges->offsetSet($sender, $propertyChanges); + } + + /** + * {@inheritDoc} + */ + public function updateAcl(MutableAclInterface $acl) + { + if (!$this->propertyChanges->contains($acl)) { + throw new \InvalidArgumentException('$acl is not tracked by this provider.'); + } + + $propertyChanges = $this->propertyChanges->offsetGet($acl); + // check if any changes were made to this ACL + if (0 === count($propertyChanges)) { + return; + } + + $sets = $sharedPropertyChanges = array(); + + $this->connection->beginTransaction(); + try { + if (isset($propertyChanges['entriesInheriting'])) { + $sets[] = 'entries_inheriting = '.$this->connection->getDatabasePlatform()->convertBooleans($propertyChanges['entriesInheriting'][1]); + } + + if (isset($propertyChanges['parentAcl'])) { + if (null === $propertyChanges['parentAcl'][1]) { + $sets[] = 'parent_object_identity_id = NULL'; + } else { + $sets[] = 'parent_object_identity_id = '.intval($propertyChanges['parentAcl'][1]->getId()); + } + + $this->regenerateAncestorRelations($acl); + } + + // this includes only updates of existing ACEs, but neither the creation, nor + // the deletion of ACEs; these are tracked by changes to the ACL's respective + // properties (classAces, classFieldAces, objectAces, objectFieldAces) + if (isset($propertyChanges['aces'])) { + $this->updateAces($propertyChanges['aces']); + } + + // check properties for deleted, and created ACEs + if (isset($propertyChanges['classAces'])) { + $this->updateAceProperty('classAces', $propertyChanges['classAces']); + $sharedPropertyChanges['classAces'] = $propertyChanges['classAces']; + } + if (isset($propertyChanges['classFieldAces'])) { + $this->updateFieldAceProperty('classFieldAces', $propertyChanges['classFieldAces']); + $sharedPropertyChanges['classFieldAces'] = $propertyChanges['classFieldAces']; + } + if (isset($propertyChanges['objectAces'])) { + $this->updateAceProperty('objectAces', $propertyChanges['objectAces']); + } + if (isset($propertyChanges['objectFieldAces'])) { + $this->updateFieldAceProperty('objectFieldAces', $propertyChanges['objectFieldAces']); + } + + // if there have been changes to shared properties, we need to synchronize other + // ACL instances for object identities of the same type that are already in-memory + if (count($sharedPropertyChanges) > 0) { + $classAcesProperty = new \ReflectionProperty('Symfony\Component\Security\Acl\Domain\Acl', 'classAces'); + $classAcesProperty->setAccessible(true); + $classFieldAcesProperty = new \ReflectionProperty('Symfony\Component\Security\Acl\Domain\Acl', 'classFieldAces'); + $classFieldAcesProperty->setAccessible(true); + + foreach ($this->loadedAcls[$acl->getObjectIdentity()->getType()] as $sameTypeAcl) { + if (isset($sharedPropertyChanges['classAces'])) { + if ($acl !== $sameTypeAcl && $classAcesProperty->getValue($sameTypeAcl) !== $sharedPropertyChanges['classAces'][0]) { + throw new ConcurrentModificationException('The "classAces" property has been modified concurrently.'); + } + + $classAcesProperty->setValue($sameTypeAcl, $sharedPropertyChanges['classAces'][1]); + } + + if (isset($sharedPropertyChanges['classFieldAces'])) { + if ($acl !== $sameTypeAcl && $classFieldAcesProperty->getValue($sameTypeAcl) !== $sharedPropertyChanges['classFieldAces'][0]) { + throw new ConcurrentModificationException('The "classFieldAces" property has been modified concurrently.'); + } + + $classFieldAcesProperty->setValue($sameTypeAcl, $sharedPropertyChanges['classFieldAces'][1]); + } + } + } + + // persist any changes to the acl_object_identities table + if (count($sets) > 0) { + $this->connection->executeQuery($this->getUpdateObjectIdentitySql($acl->getId(), $sets)); + } + + $this->connection->commit(); + } catch (\Exception $failed) { + $this->connection->rollBack(); + + throw $failed; + } + + $this->propertyChanges->offsetSet($acl, array()); + + if (null !== $this->aclCache) { + if (count($sharedPropertyChanges) > 0) { + // FIXME: Currently, there is no easy way to clear the cache for ACLs + // of a certain type. The problem here is that we need to make + // sure to clear the cache of all child ACLs as well, and these + // child ACLs might be of a different class type. + $this->aclCache->clearCache(); + } else { + // if there are no shared property changes, it's sufficient to just delete + // the cache for this ACL + $this->aclCache->evictFromCacheByIdentity($acl->getObjectIdentity()); + + foreach ($this->findChildren($acl->getObjectIdentity()) as $childOid) { + $this->aclCache->evictFromCacheByIdentity($childOid); + } + } + } + } + + /** + * Creates the ACL for the passed object identity + * + * @param ObjectIdentityInterface $oid + * @return void + */ + protected function createObjectIdentity(ObjectIdentityInterface $oid) + { + $classId = $this->createOrRetrieveClassId($oid->getType()); + + $this->connection->executeQuery($this->getInsertObjectIdentitySql($oid->getIdentifier(), $classId, true)); + } + + /** + * Returns the primary key for the passed class type. + * + * If the type does not yet exist in the database, it will be created. + * + * @param string $classType + * @return integer + */ + protected function createOrRetrieveClassId($classType) + { + if (false !== $id = $this->connection->executeQuery($this->getSelectClassIdSql($classType))->fetchColumn()) { + return $id; + } + + $this->connection->executeQuery($this->getInsertClassSql($classType)); + + return $this->connection->executeQuery($this->getSelectClassIdSql($classType))->fetchColumn(); + } + + /** + * Returns the primary key for the passed security identity. + * + * If the security identity does not yet exist in the database, it will be + * created. + * + * @param SecurityIdentityInterface $sid + * @return integer + */ + protected function createOrRetrieveSecurityIdentityId(SecurityIdentityInterface $sid) + { + if (false !== $id = $this->connection->executeQuery($this->getSelectSecurityIdentityIdSql($sid))->fetchColumn()) { + return $id; + } + + $this->connection->executeQuery($this->getInsertSecurityIdentitySql($sid)); + + return $this->connection->executeQuery($this->getSelectSecurityIdentityIdSql($sid))->fetchColumn(); + } + + /** + * Deletes all ACEs for the given object identity primary key. + * + * @param integer $oidPK + * @return void + */ + protected function deleteAccessControlEntries($oidPK) + { + $this->connection->executeQuery($this->getDeleteAccessControlEntriesSql($oidPK)); + } + + /** + * Deletes the object identity from the database. + * + * @param integer $pk + * @return void + */ + protected function deleteObjectIdentity($pk) + { + $this->connection->executeQuery($this->getDeleteObjectIdentitySql($pk)); + } + + /** + * Deletes all entries from the relations table from the database. + * + * @param integer $pk + * @return void + */ + protected function deleteObjectIdentityRelations($pk) + { + $this->connection->executeQuery($this->getDeleteObjectIdentityRelationsSql($pk)); + } + + /** + * Constructs the SQL for deleting access control entries. + * + * @param integer $oidPK + * @return string + */ + protected function getDeleteAccessControlEntriesSql($oidPK) + { + return sprintf( + 'DELETE FROM %s WHERE object_identity_id = %d', + $this->options['entry_table_name'], + $oidPK + ); + } + + /** + * Constructs the SQL for deleting a specific ACE. + * + * @param integer $acePK + * @return string + */ + protected function getDeleteAccessControlEntrySql($acePK) + { + return sprintf( + 'DELETE FROM %s WHERE id = %d', + $this->options['entry_table_name'], + $acePK + ); + } + + /** + * Constructs the SQL for deleting an object identity. + * + * @param integer $pk + * @return string + */ + protected function getDeleteObjectIdentitySql($pk) + { + return sprintf( + 'DELETE FROM %s WHERE id = %d', + $this->options['oid_table_name'], + $pk + ); + } + + /** + * Constructs the SQL for deleting relation entries. + * + * @param integer $pk + * @return string + */ + protected function getDeleteObjectIdentityRelationsSql($pk) + { + return sprintf( + 'DELETE FROM %s WHERE object_identity_id = %d', + $this->options['oid_ancestors_table_name'], + $pk + ); + } + + /** + * Constructs the SQL for inserting an ACE. + * + * @param integer $classId + * @param integer|null $objectIdentityId + * @param string|null $field + * @param integer $aceOrder + * @param integer $securityIdentityId + * @param string $strategy + * @param integer $mask + * @param Boolean $granting + * @param Boolean $auditSuccess + * @param Boolean $auditFailure + * @return string + */ + protected function getInsertAccessControlEntrySql($classId, $objectIdentityId, $field, $aceOrder, $securityIdentityId, $strategy, $mask, $granting, $auditSuccess, $auditFailure) + { + $query = <<options['entry_table_name'], + $classId, + null === $objectIdentityId? 'NULL' : intval($objectIdentityId), + null === $field? 'NULL' : $this->connection->quote($field), + $aceOrder, + $securityIdentityId, + $mask, + $this->connection->getDatabasePlatform()->convertBooleans($granting), + $this->connection->quote($strategy), + $this->connection->getDatabasePlatform()->convertBooleans($auditSuccess), + $this->connection->getDatabasePlatform()->convertBooleans($auditFailure) + ); + } + + /** + * Constructs the SQL for inserting a new class type. + * + * @param string $classType + * @return string + */ + protected function getInsertClassSql($classType) + { + return sprintf( + 'INSERT INTO %s (class_type) VALUES (%s)', + $this->options['class_table_name'], + $this->connection->quote($classType) + ); + } + + /** + * Constructs the SQL for inserting a relation entry. + * + * @param integer $objectIdentityId + * @param integer $ancestorId + * @return string + */ + protected function getInsertObjectIdentityRelationSql($objectIdentityId, $ancestorId) + { + return sprintf( + 'INSERT INTO %s (object_identity_id, ancestor_id) VALUES (%d, %d)', + $this->options['oid_ancestors_table_name'], + $objectIdentityId, + $ancestorId + ); + } + + /** + * Constructs the SQL for inserting an object identity. + * + * @param string $identifier + * @param integer $classId + * @param Boolean $entriesInheriting + * @return string + */ + protected function getInsertObjectIdentitySql($identifier, $classId, $entriesInheriting) + { + $query = <<options['oid_table_name'], + $classId, + $this->connection->quote($identifier), + $this->connection->getDatabasePlatform()->convertBooleans($entriesInheriting) + ); + } + + /** + * Constructs the SQL for inserting a security identity. + * + * @param SecurityIdentityInterface $sid + * @throws \InvalidArgumentException + * @return string + */ + protected function getInsertSecurityIdentitySql(SecurityIdentityInterface $sid) + { + if ($sid instanceof UserSecurityIdentity) { + $identifier = $sid->getUsername(); + $username = true; + } else if ($sid instanceof RoleSecurityIdentity) { + $identifier = $sid->getRole(); + $username = false; + } else { + throw new \InvalidArgumentException('$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.'); + } + + return sprintf( + 'INSERT INTO %s (identifier, username) VALUES (%s, %s)', + $this->options['sid_table_name'], + $this->connection->quote($identifier), + $this->connection->getDatabasePlatform()->convertBooleans($username) + ); + } + + /** + * Constructs the SQL for selecting an ACE. + * + * @param integer $classId + * @param integer $oid + * @param string $field + * @param integer $order + * @return string + */ + protected function getSelectAccessControlEntryIdSql($classId, $oid, $field, $order) + { + return sprintf( + 'SELECT id FROM %s WHERE class_id = %d AND %s AND %s AND ace_order = %d', + $this->options['entry_table_name'], + $classId, + null === $oid ? + $this->connection->getDatabasePlatform()->getIsNullExpression('object_identity_id') + : 'object_identity_id = '.intval($oid), + null === $field ? + $this->connection->getDatabasePlatform()->getIsNullExpression('field_name') + : 'field_name = '.$this->connection->quote($field), + $order + ); + } + + /** + * Constructs the SQL for selecting the primary key associated with + * the passed class type. + * + * @param string $classType + * @return string + */ + protected function getSelectClassIdSql($classType) + { + return sprintf( + 'SELECT id FROM %s WHERE class_type = %s', + $this->options['class_table_name'], + $this->connection->quote($classType) + ); + } + + /** + * Constructs the SQL for selecting the primary key of a security identity. + * + * @param SecurityIdentityInterface $sid + * @throws \InvalidArgumentException + * @return string + */ + protected function getSelectSecurityIdentityIdSql(SecurityIdentityInterface $sid) + { + if ($sid instanceof UserSecurityIdentity) { + $identifier = $sid->getUsername(); + $username = true; + } else if ($sid instanceof RoleSecurityIdentity) { + $identifier = $sid->getRole(); + $username = false; + } else { + throw new \InvalidArgumentException('$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.'); + } + + return sprintf( + 'SELECT id FROM %s WHERE identifier = %s AND username = %s', + $this->options['sid_table_name'], + $this->connection->quote($identifier), + $this->connection->getDatabasePlatform()->convertBooleans($username) + ); + } + + /** + * Constructs the SQL for updating an object identity. + * + * @param integer $pk + * @param array $changes + * @throws \InvalidArgumentException + * @return string + */ + protected function getUpdateObjectIdentitySql($pk, array $changes) + { + if (0 === count($changes)) { + throw new \InvalidArgumentException('There are no changes.'); + } + + return sprintf( + 'UPDATE %s SET %s WHERE id = %d', + $this->options['oid_table_name'], + implode(', ', $changes), + $pk + ); + } + + /** + * Constructs the SQL for updating an ACE. + * + * @param integer $pk + * @param array $sets + * @throws \InvalidArgumentException + * @return string + */ + protected function getUpdateAccessControlEntrySql($pk, array $sets) + { + if (0 === count($sets)) { + throw new \InvalidArgumentException('There are no changes.'); + } + + return sprintf( + 'UPDATE %s SET %s WHERE id = %d', + $this->options['entry_table_name'], + implode(', ', $sets), + $pk + ); + } + + /** + * This regenerates the ancestor table which is used for fast read access. + * + * @param AclInterface $acl + * @return void + */ + protected function regenerateAncestorRelations(AclInterface $acl) + { + $pk = $acl->getId(); + $this->connection->executeQuery($this->getDeleteObjectIdentityRelationsSql($pk)); + $this->connection->executeQuery($this->getInsertObjectIdentityRelationSql($pk, $pk)); + + $parentAcl = $acl->getParentAcl(); + while (null !== $parentAcl) { + $this->connection->executeQuery($this->getInsertObjectIdentityRelationSql($pk, $parentAcl->getId())); + + $parentAcl = $parentAcl->getParentAcl(); + } + } + + /** + * This processes changes on an ACE related property (classFieldAces, or objectFieldAces). + * + * @param string $name + * @param array $changes + * @return void + */ + protected function updateFieldAceProperty($name, array $changes) + { + $sids = new \SplObjectStorage(); + $classIds = new \SplObjectStorage(); + $currentIds = array(); + foreach ($changes[1] as $field => $new) { + for ($i=0,$c=count($new); $i<$c; $i++) { + $ace = $new[$i]; + + if (null === $ace->getId()) { + if ($sids->contains($ace->getSecurityIdentity())) { + $sid = $sids->offsetGet($ace->getSecurityIdentity()); + } else { + $sid = $this->createOrRetrieveSecurityIdentityId($ace->getSecurityIdentity()); + } + + $oid = $ace->getAcl()->getObjectIdentity(); + if ($classIds->contains($oid)) { + $classId = $classIds->offsetGet($oid); + } else { + $classId = $this->createOrRetrieveClassId($oid->getType()); + } + + $objectIdentityId = $name === 'classFieldAces' ? null : $ace->getAcl()->getId(); + + $this->connection->executeQuery($this->getInsertAccessControlEntrySql($classId, $objectIdentityId, $field, $i, $sid, $ace->getStrategy(), $ace->getMask(), $ace->isGranting(), $ace->isAuditSuccess(), $ace->isAuditFailure())); + $aceId = $this->connection->executeQuery($this->getSelectAccessControlEntryIdSql($classId, $objectIdentityId, $field, $i))->fetchColumn(); + $this->loadedAces[$aceId] = $ace; + + $aceIdProperty = new \ReflectionProperty($ace, 'id'); + $aceIdProperty->setAccessible(true); + $aceIdProperty->setValue($ace, intval($aceId)); + } else { + $currentIds[$ace->getId()] = true; + } + } + } + + foreach ($changes[0] as $field => $old) { + for ($i=0,$c=count($old); $i<$c; $i++) { + $ace = $old[$i]; + + if (!isset($currentIds[$ace->getId()])) { + $this->connection->executeQuery($this->getDeleteAccessControlEntrySql($ace->getId())); + unset($this->loadedAces[$ace->getId()]); + } + } + } + } + + /** + * This processes changes on an ACE related property (classAces, or objectAces). + * + * @param string $name + * @param array $changes + * @return void + */ + protected function updateAceProperty($name, array $changes) + { + list($old, $new) = $changes; + + $sids = new \SplObjectStorage(); + $classIds = new \SplObjectStorage(); + $currentIds = array(); + for ($i=0,$c=count($new); $i<$c; $i++) { + $ace = $new[$i]; + + if (null === $ace->getId()) { + if ($sids->contains($ace->getSecurityIdentity())) { + $sid = $sids->offsetGet($ace->getSecurityIdentity()); + } else { + $sid = $this->createOrRetrieveSecurityIdentityId($ace->getSecurityIdentity()); + } + + $oid = $ace->getAcl()->getObjectIdentity(); + if ($classIds->contains($oid)) { + $classId = $classIds->offsetGet($oid); + } else { + $classId = $this->createOrRetrieveClassId($oid->getType()); + } + + $objectIdentityId = $name === 'classAces' ? null : $ace->getAcl()->getId(); + + $this->connection->executeQuery($this->getInsertAccessControlEntrySql($classId, $objectIdentityId, null, $i, $sid, $ace->getStrategy(), $ace->getMask(), $ace->isGranting(), $ace->isAuditSuccess(), $ace->isAuditFailure())); + $aceId = $this->connection->executeQuery($this->getSelectAccessControlEntryIdSql($classId, $objectIdentityId, null, $i))->fetchColumn(); + $this->loadedAces[$aceId] = $ace; + + $aceIdProperty = new \ReflectionProperty($ace, 'id'); + $aceIdProperty->setAccessible(true); + $aceIdProperty->setValue($ace, intval($aceId)); + } else { + $currentIds[$ace->getId()] = true; + } + } + + for ($i=0,$c=count($old); $i<$c; $i++) { + $ace = $old[$i]; + + if (!isset($currentIds[$ace->getId()])) { + $this->connection->executeQuery($this->getDeleteAccessControlEntrySql($ace->getId())); + unset($this->loadedAces[$ace->getId()]); + } + } + } + + /** + * Persists the changes which were made to ACEs to the database. + * + * @param \SplObjectStorage $aces + * @return void + */ + protected function updateAces(\SplObjectStorage $aces) + { + foreach ($aces as $ace) + { + $propertyChanges = $aces->offsetGet($ace); + $sets = array(); + + if (isset($propertyChanges['mask'])) { + $sets[] = sprintf('mask = %d', $propertyChanges['mask'][1]); + } + if (isset($propertyChanges['strategy'])) { + $sets[] = sprintf('granting_strategy = %s', $this->connection->quote($propertyChanges['strategy'])); + } + if (isset($propertyChanges['aceOrder'])) { + $sets[] = sprintf('ace_order = %d', $propertyChanges['aceOrder'][1]); + } + if (isset($propertyChanges['auditSuccess'])) { + $sets[] = sprintf('audit_success = %s', $this->connection->getDatabasePlatform()->convertBooleans($propertyChanges['auditSuccess'][1])); + } + if (isset($propertyChanges['auditFailure'])) { + $sets[] = sprintf('audit_failure = %s', $this->connection->getDatabasePlatform()->convertBooleans($propertyChanges['auditFailure'][1])); + } + + $this->connection->executeQuery($this->getUpdateAccessControlEntrySql($ace->getId(), $sets)); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Dbal/Schema.php b/src/Symfony/Component/Security/Acl/Dbal/Schema.php new file mode 100644 index 000000000000..16959444ef37 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Dbal/Schema.php @@ -0,0 +1,145 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * The schema used for the ACL system. + * + * @author Johannes M. Schmitt + */ +class Schema extends BaseSchema +{ + protected $options; + + /** + * Constructor + * + * @param array $options the names for tables + * @return void + */ + public function __construct(array $options) + { + parent::__construct(); + + $this->options = $options; + + $this->addClassTable(); + $this->addSecurityIdentitiesTable(); + $this->addObjectIdentitiesTable(); + $this->addObjectIdentityAncestorsTable(); + $this->addEntryTable(); + } + + /** + * Adds the class table to the schema + * + * @return void + */ + protected function addClassTable() + { + $table = $this->createTable($this->options['class_table_name']); + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_type', 'string', array('length' => 200)); + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('class_type')); + } + + /** + * Adds the entry table to the schema + * + * @return void + */ + protected function addEntryTable() + { + $table = $this->createTable($this->options['entry_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_id', 'integer', array('unsigned' => true)); + $table->addColumn('object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); + $table->addColumn('field_name', 'string', array('length' => 50, 'notnull' => false)); + $table->addColumn('ace_order', 'smallint', array('unsigned' => true)); + $table->addColumn('security_identity_id', 'integer', array('unsigned' => true)); + $table->addColumn('mask', 'integer'); + $table->addColumn('granting', 'boolean'); + $table->addColumn('granting_strategy', 'string', array('length' => 30)); + $table->addColumn('audit_success', 'boolean', array('default' => 0)); + $table->addColumn('audit_failure', 'boolean', array('default' => 0)); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('class_id', 'object_identity_id', 'field_name', 'ace_order')); + $table->addIndex(array('class_id', 'object_identity_id', 'security_identity_id')); + + $table->addForeignKeyConstraint($this->getTable($this->options['class_table_name']), array('class_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($this->getTable($this->options['oid_table_name']), array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($this->getTable($this->options['sid_table_name']), array('security_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + } + + /** + * Adds the object identity table to the schema + * + * @return void + */ + protected function addObjectIdentitiesTable() + { + $table = $this->createTable($this->options['oid_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_id', 'integer', array('unsigned' => true)); + $table->addColumn('object_identifier', 'string', array('length' => 100)); + $table->addColumn('parent_object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); + $table->addColumn('entries_inheriting', 'boolean', array('default' => 0)); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('object_identifier', 'class_id')); + $table->addIndex(array('parent_object_identity_id')); + + $table->addForeignKeyConstraint($table, array('parent_object_identity_id'), array('id'), array('onDelete' => 'RESTRICT', 'onUpdate' => 'RESTRICT')); + } + + /** + * Adds the object identity relation table to the schema + * + * @return void + */ + protected function addObjectIdentityAncestorsTable() + { + $table = $this->createTable($this->options['oid_ancestors_table_name']); + + $table->addColumn('object_identity_id', 'integer', array('unsigned' => true)); + $table->addColumn('ancestor_id', 'integer', array('unsigned' => true)); + + $table->setPrimaryKey(array('object_identity_id', 'ancestor_id')); + + $oidTable = $this->getTable($this->options['oid_table_name']); + $table->addForeignKeyConstraint($oidTable, array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($oidTable, array('ancestor_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + } + + /** + * Adds the security identity table to the schema + * + * @return void + */ + protected function addSecurityIdentitiesTable() + { + $table = $this->createTable($this->options['sid_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('identifier', 'string', array('length' => 100)); + $table->addColumn('username', 'boolean', array('default' => 0)); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('identifier', 'username')); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/Acl.php b/src/Symfony/Component/Security/Acl/Domain/Acl.php new file mode 100644 index 000000000000..c0c983071400 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/Acl.php @@ -0,0 +1,679 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * An ACL implementation. + * + * Each object identity has exactly one associated ACL. Each ACL can have four + * different types of ACEs (class ACEs, object ACEs, class field ACEs, object field + * ACEs). + * + * You should not iterate over the ACEs yourself, but instead use isGranted(), + * or isFieldGranted(). These will utilize an implementation of PermissionGrantingStrategy + * internally. + * + * @author Johannes M. Schmitt + */ +class Acl implements AuditableAclInterface +{ + protected $parentAcl; + protected $permissionGrantingStrategy; + protected $objectIdentity; + protected $classAces; + protected $classFieldAces; + protected $objectAces; + protected $objectFieldAces; + protected $id; + protected $loadedSids; + protected $entriesInheriting; + protected $listeners; + + /** + * Constructor + * + * @param integer $id + * @param ObjectIdentityInterface $objectIdentity + * @param PermissionGrantingStrategyInterface $permissionGrantingStrategy + * @param array $loadedSids + * @param Boolean $entriesInheriting + * @return void + */ + public function __construct($id, ObjectIdentityInterface $objectIdentity, PermissionGrantingStrategyInterface $permissionGrantingStrategy, array $loadedSids = array(), $entriesInheriting) + { + $this->id = $id; + $this->objectIdentity = $objectIdentity; + $this->permissionGrantingStrategy = $permissionGrantingStrategy; + $this->loadedSids = $loadedSids; + $this->entriesInheriting = $entriesInheriting; + $this->parentAcl = null; + $this->classAces = array(); + $this->classFieldAces = array(); + $this->objectAces = array(); + $this->objectFieldAces = array(); + $this->listeners = array(); + } + + /** + * Adds a property changed listener + * + * @param PropertyChangedListener $listener + * @return void + */ + public function addPropertyChangedListener(PropertyChangedListener $listener) + { + $this->listeners[] = $listener; + } + + /** + * {@inheritDoc} + */ + public function deleteClassAce($index) + { + $this->deleteAce('classAces', $index); + } + + /** + * {@inheritDoc} + */ + public function deleteClassFieldAce($index, $field) + { + $this->deleteFieldAce('classFieldAces', $index, $field); + } + + /** + * {@inheritDoc} + */ + public function deleteObjectAce($index) + { + $this->deleteAce('objectAces', $index); + } + + /** + * {@inheritDoc} + */ + public function deleteObjectFieldAce($index, $field) + { + $this->deleteFieldAce('objectFieldAces', $index, $field); + } + + /** + * {@inheritDoc} + */ + public function getClassAces() + { + return $this->classAces; + } + + /** + * {@inheritDoc} + */ + public function getClassFieldAces($field) + { + return isset($this->classFieldAces[$field])? $this->classFieldAces[$field] : array(); + } + + /** + * {@inheritDoc} + */ + public function getObjectAces() + { + return $this->objectAces; + } + + /** + * {@inheritDoc} + */ + public function getObjectFieldAces($field) + { + return isset($this->objectFieldAces[$field]) ? $this->objectFieldAces[$field] : array(); + } + + /** + * {@inheritDoc} + */ + public function getId() + { + return $this->id; + } + + /** + * {@inheritDoc} + */ + public function getObjectIdentity() + { + return $this->objectIdentity; + } + + /** + * {@inheritDoc} + */ + public function getParentAcl() + { + return $this->parentAcl; + } + + /** + * {@inheritDoc} + */ + public function insertClassAce(SecurityIdentityInterface $sid, $mask, $index = 0, $granting = true, $strategy = null) + { + $this->insertAce('classAces', $index, $mask, $sid, $granting, $strategy); + } + + /** + * {@inheritDoc} + */ + public function insertClassFieldAce($field, SecurityIdentityInterface $sid, $mask, $index = 0, $granting = true, $strategy = null) + { + $this->insertFieldAce('classFieldAces', $index, $field, $mask, $sid, $granting, $strategy); + } + + /** + * {@inheritDoc} + */ + public function insertObjectAce(SecurityIdentityInterface $sid, $mask, $index = 0, $granting = true, $strategy = null) + { + $this->insertAce('objectAces', $index, $mask, $sid, $granting, $strategy); + } + + /** + * {@inheritDoc} + */ + public function insertObjectFieldAce($field, SecurityIdentityInterface $sid, $mask, $index = 0, $granting = true, $strategy = null) + { + $this->insertFieldAce('objectFieldAces', $index, $field, $mask, $sid, $granting, $strategy); + } + + /** + * {@inheritDoc} + */ + public function isEntriesInheriting() + { + return $this->entriesInheriting; + } + + /** + * {@inheritDoc} + */ + public function isFieldGranted($field, array $masks, array $securityIdentities, $administrativeMode = false) + { + return $this->permissionGrantingStrategy->isFieldGranted($this, $field, $masks, $securityIdentities, $administrativeMode); + } + + /** + * {@inheritDoc} + */ + public function isGranted(array $masks, array $securityIdentities, $administrativeMode = false) + { + return $this->permissionGrantingStrategy->isGranted($this, $masks, $securityIdentities, $administrativeMode); + } + + /** + * {@inheritDoc} + */ + public function isSidLoaded($sids) + { + if (0 === count($this->loadedSids)) { + return true; + } + + if (!is_array($sids)) { + $sids = array($sids); + } + + foreach ($sids as $sid) { + if (!$sid instanceof SecurityIdentityInterface) { + throw new \InvalidArgumentException( + '$sid must be an instance of SecurityIdentityInterface.'); + } + + foreach ($this->loadedSids as $loadedSid) { + if ($loadedSid->equals($sid)) { + continue 2; + } + } + + return false; + } + + return true; + } + + /** + * Implementation for the \Serializable interface + * + * @return string + */ + public function serialize() + { + return serialize(array( + null === $this->parentAcl ? null : $this->parentAcl->getId(), + $this->objectIdentity, + $this->classAces, + $this->classFieldAces, + $this->objectAces, + $this->objectFieldAces, + $this->id, + $this->loadedSids, + $this->entriesInheriting, + )); + } + + /** + * Implementation for the \Serializable interface + * + * @param string $serialized + * @return void + */ + public function unserialize($serialized) + { + list($this->parentAcl, + $this->objectIdentity, + $this->classAces, + $this->classFieldAces, + $this->objectAces, + $this->objectFieldAces, + $this->id, + $this->loadedSids, + $this->entriesInheriting + ) = unserialize($serialized); + + $this->listeners = array(); + } + + /** + * {@inheritDoc} + */ + public function setEntriesInheriting($boolean) + { + if ($this->entriesInheriting !== $boolean) { + $this->onPropertyChanged('entriesInheriting', $this->entriesInheriting, $boolean); + $this->entriesInheriting = $boolean; + } + } + + /** + * {@inheritDoc} + */ + public function setParentAcl(AclInterface $acl) + { + if (null === $acl->getId()) { + throw new \InvalidArgumentException('$acl must have an ID.'); + } + + if ($this->parentAcl !== $acl) { + $this->onPropertyChanged('parentAcl', $this->parentAcl, $acl); + $this->parentAcl = $acl; + } + } + + /** + * {@inheritDoc} + */ + public function updateClassAce($index, $mask, $strategy = null) + { + $this->updateAce('classAces', $index, $mask, $strategy); + } + + /** + * {@inheritDoc} + */ + public function updateClassFieldAce($index, $field, $mask, $strategy = null) + { + $this->updateFieldAce('classFieldAces', $index, $field, $mask, $strategy); + } + + /** + * {@inheritDoc} + */ + public function updateObjectAce($index, $mask, $strategy = null) + { + $this->updateAce('objectAces', $index, $mask, $strategy); + } + + /** + * {@inheritDoc} + */ + public function updateObjectFieldAce($index, $field, $mask, $strategy = null) + { + $this->updateFieldAce('objectFieldAces', $index, $field, $mask, $strategy); + } + + /** + * {@inheritDoc} + */ + public function updateClassAuditing($index, $auditSuccess, $auditFailure) + { + $this->updateAuditing($this->classAces, $index, $auditSuccess, $auditFailure); + } + + /** + * {@inheritDoc} + */ + public function updateClassFieldAuditing($index, $field, $auditSuccess, $auditFailure) + { + if (!isset($this->classFieldAces[$field])) { + throw new \InvalidArgumentException(sprintf('There are no ACEs for field "%s".', $field)); + } + + $this->updateAuditing($this->classFieldAces[$field], $index, $auditSuccess, $auditFailure); + } + + /** + * {@inheritDoc} + */ + public function updateObjectAuditing($index, $auditSuccess, $auditFailure) + { + $this->updateAuditing($this->objectAces, $index, $auditSuccess, $auditFailure); + } + + /** + * {@inheritDoc} + */ + public function updateObjectFieldAuditing($index, $field, $auditSuccess, $auditFailure) + { + if (!isset($this->objectFieldAces[$field])) { + throw new \InvalidArgumentException(sprintf('There are no ACEs for field "%s".', $field)); + } + + $this->updateAuditing($this->objectFieldAces[$field], $index, $auditSuccess, $auditFailure); + } + + /** + * Deletes an ACE + * + * @param string $property + * @param integer $index + * @throws \OutOfBoundsException + * @return void + */ + protected function deleteAce($property, $index) + { + $aces =& $this->$property; + if (!isset($aces[$index])) { + throw new \OutOfBoundsException(sprintf('The index "%d" does not exist.', $index)); + } + + $oldValue = $this->$property; + unset($aces[$index]); + $this->$property = array_values($this->$property); + $this->onPropertyChanged($property, $oldValue, $this->$property); + + for ($i=$index,$c=count($this->$property); $i<$c; $i++) { + $this->onEntryPropertyChanged($aces[$i], 'aceOrder', $i+1, $i); + } + } + + /** + * Deletes a field-based ACE + * + * @param string $property + * @param integer $index + * @param string $field + * @throws \OutOfBoundsException + * @return void + */ + protected function deleteFieldAce($property, $index, $field) + { + $aces =& $this->$property; + if (!isset($aces[$field][$index])) { + throw new \OutOfBoundsException(sprintf('The index "%d" does not exist.', $index)); + } + + $oldValue = $this->$property; + unset($aces[$field][$index]); + $aces[$field] = array_values($aces[$field]); + $this->onPropertyChanged($property, $oldValue, $this->$property); + + for ($i=$index,$c=count($aces[$field]); $i<$c; $i++) { + $this->onEntryPropertyChanged($aces[$field][$i], 'aceOrder', $i+1, $i); + } + } + + /** + * Inserts an ACE + * + * @param string $property + * @param integer $index + * @param integer $mask + * @param SecurityIdentityInterface $sid + * @param Boolean $granting + * @param string $strategy + * @throws \OutOfBoundsException + * @throws \InvalidArgumentException + * @return void + */ + protected function insertAce($property, $index, $mask, SecurityIdentityInterface $sid, $granting, $strategy = null) + { + if ($index < 0 || $index > count($this->$property)) { + throw new \OutOfBoundsException(sprintf('The index must be in the interval [0, %d].', count($this->$property))); + } + + if (!is_int($mask)) { + throw new \InvalidArgumentException('$mask must be an integer.'); + } + + if (null === $strategy) { + if (true === $granting) { + $strategy = PermissionGrantingStrategy::ALL; + } else { + $strategy = PermissionGrantingStrategy::ANY; + } + } + + $aces =& $this->$property; + $oldValue = $this->$property; + if (isset($aces[$index])) { + $this->$property = array_merge( + array_slice($this->$property, 0, $index), + array(true), + array_slice($this->$property, $index) + ); + + for ($i=$index,$c=count($this->$property)-1; $i<$c; $i++) { + $this->onEntryPropertyChanged($aces[$i+1], 'aceOrder', $i, $i+1); + } + } + + $aces[$index] = new Entry(null, $this, $sid, $strategy, $mask, $granting, false, false); + $this->onPropertyChanged($property, $oldValue, $this->$property); + } + + /** + * Inserts a field-based ACE + * + * @param string $property + * @param integer $index + * @param string $field + * @param integer $mask + * @param SecurityIdentityInterface $sid + * @param Boolean $granting + * @param string $strategy + * @throws \InvalidArgumentException + * @throws \OutOfBoundsException + * @return void + */ + protected function insertFieldAce($property, $index, $field, $mask, SecurityIdentityInterface $sid, $granting, $strategy = null) + { + if (0 === strlen($field)) { + throw new \InvalidArgumentException('$field cannot be empty.'); + } + + if (!is_int($mask)) { + throw new \InvalidArgumentException('$mask must be an integer.'); + } + + if (null === $strategy) { + if (true === $granting) { + $strategy = PermissionGrantingStrategy::ALL; + } else { + $strategy = PermissionGrantingStrategy::ANY; + } + } + + $aces =& $this->$property; + if (!isset($aces[$field])) { + $aces[$field] = array(); + } + + if ($index < 0 || $index > count($aces[$field])) { + throw new \OutOfBoundsException(sprintf('The index must be in the interval [0, %d].', count($this->$property))); + } + + $oldValue = $aces; + if (isset($aces[$field][$index])) { + $aces[$field] = array_merge( + array_slice($aces[$field], 0, $index), + array(true), + array_slice($aces[$field], $index) + ); + + for ($i=$index,$c=count($aces[$field])-1; $i<$c; $i++) { + $this->onEntryPropertyChanged($aces[$field][$i+1], 'aceOrder', $i, $i+1); + } + } + + $aces[$field][$index] = new FieldEntry(null, $this, $field, $sid, $strategy, $mask, $granting, false, false); + $this->onPropertyChanged($property, $oldValue, $this->$property); + } + + /** + * Called when a property of the ACL changes + * + * @param string $name + * @param mixed $oldValue + * @param mixed $newValue + * @return void + */ + protected function onPropertyChanged($name, $oldValue, $newValue) + { + foreach ($this->listeners as $listener) { + $listener->propertyChanged($this, $name, $oldValue, $newValue); + } + } + + /** + * Called when a property of an ACE associated with this ACL changes + * + * @param EntryInterface $entry + * @param string $name + * @param mixed $oldValue + * @param mixed $newValue + * @return void + */ + protected function onEntryPropertyChanged(EntryInterface $entry, $name, $oldValue, $newValue) + { + foreach ($this->listeners as $listener) { + $listener->propertyChanged($entry, $name, $oldValue, $newValue); + } + } + + /** + * Updates an ACE + * + * @param string $property + * @param integer $index + * @param integer $mask + * @param string $strategy + * @throws \OutOfBoundsException + * @return void + */ + protected function updateAce($property, $index, $mask, $strategy = null) + { + $aces =& $this->$property; + if (!isset($aces[$index])) { + throw new \OutOfBoundsException(sprintf('The index "%d" does not exist.', $index)); + } + + $ace = $aces[$index]; + if ($mask !== $oldMask = $ace->getMask()) { + $this->onEntryPropertyChanged($ace, 'mask', $oldMask, $mask); + $ace->setMask($mask); + } + if (null !== $strategy && $strategy !== $oldStrategy = $ace->getStrategy()) { + $this->onEntryPropertyChanged($ace, 'strategy', $oldStrategy, $strategy); + $ace->setStrategy($strategy); + } + } + + /** + * Updates auditing for an ACE + * + * @param array $aces + * @param integer $index + * @param Boolean $auditSuccess + * @param Boolean $auditFailure + * @throws \OutOfBoundsException + * @return void + */ + protected function updateAuditing(array &$aces, $index, $auditSuccess, $auditFailure) + { + if (!isset($aces[$index])) { + throw new \OutOfBoundsException(sprintf('The index "%d" does not exist.', $index)); + } + + if ($auditSuccess !== $aces[$index]->isAuditSuccess()) { + $this->onEntryPropertyChanged($aces[$index], 'auditSuccess', !$auditSuccess, $auditSuccess); + $aces[$index]->setAuditSuccess($auditSuccess); + } + + if ($auditFailure !== $aces[$index]->isAuditFailure()) { + $this->onEntryPropertyChanged($aces[$index], 'auditFailure', !$auditFailure, $auditFailure); + $aces[$index]->setAuditFailure($auditFailure); + } + } + + /** + * Updates a field-based ACE + * + * @param string $property + * @param integer $index + * @param string $field + * @param integer $mask + * @param string $strategy + * @throws \InvalidArgumentException + * @throws \OutOfBoundsException + * @return void + */ + protected function updateFieldAce($property, $index, $field, $mask, $strategy = null) + { + if (0 === strlen($field)) { + throw new \InvalidArgumentException('$field cannot be empty.'); + } + + $aces =& $this->$property; + if (!isset($aces[$field][$index])) { + throw new \OutOfBoundsException(sprintf('The index "%d" does not exist.', $index)); + } + + $ace = $aces[$field][$index]; + if ($mask !== $oldMask = $ace->getMask()) { + $this->onEntryPropertyChanged($ace, 'mask', $oldMask, $mask); + $ace->setMask($mask); + } + if (null !== $strategy && $strategy !== $oldStrategy = $ace->getStrategy()) { + $this->onEntryPropertyChanged($ace, 'strategy', $oldStrategy, $strategy); + $ace->setStrategy($strategy); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/AuditLogger.php b/src/Symfony/Component/Security/Acl/Domain/AuditLogger.php new file mode 100644 index 000000000000..12faa4c2be87 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/AuditLogger.php @@ -0,0 +1,53 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Base audit logger implementation + * + * @author Johannes M. Schmitt + */ +abstract class AuditLogger implements AuditLoggerInterface +{ + /** + * Performs some checks if logging was requested + * + * @param Boolean $granted + * @param EntryInterface $ace + * @return void + */ + public function logIfNeeded($granted, EntryInterface $ace) + { + if (!$ace instanceof AuditableEntryInterface) { + return; + } + + if ($granted && $ace->isAuditSuccess()) { + $this->doLog($granted, $ace); + } else if (!$granted && $ace->isAuditFailure()) { + $this->doLog($granted, $ace); + } + } + + /** + * This method is only called when logging is needed + * + * @param Boolean $granted + * @param EntryInterface $ace + * @return void + */ + abstract protected function doLog($granted, EntryInterface $ace); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/DoctrineAclCache.php b/src/Symfony/Component/Security/Acl/Domain/DoctrineAclCache.php new file mode 100644 index 000000000000..c6ad999ada3e --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/DoctrineAclCache.php @@ -0,0 +1,222 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This class is a wrapper around the actual cache implementation. + * + * @author Johannes M. Schmitt + */ +class DoctrineAclCache implements AclCacheInterface +{ + const PREFIX = 'sf2_acl_'; + + protected $cache; + protected $prefix; + protected $permissionGrantingStrategy; + + /** + * Constructor + * + * @param Cache $cache + * @param PermissionGrantingStrategyInterface $permissionGrantingStrategy + * @param string $prefix + * @return void + */ + public function __construct(Cache $cache, PermissionGrantingStrategyInterface $permissionGrantingStrategy, $prefix = self::PREFIX) + { + if (0 === strlen($prefix)) { + throw new \InvalidArgumentException('$prefix cannot be empty.'); + } + + $this->cache = $cache; + $this->permissionGrantingStrategy = $permissionGrantingStrategy; + $this->prefix = $prefix; + } + + /** + * {@inheritDoc} + */ + public function clearCache() + { + $this->cache->deleteByPrefix($this->prefix); + } + + /** + * {@inheritDoc} + */ + public function evictFromCacheById($aclId) + { + $lookupKey = $this->getAliasKeyForIdentity($aclId); + if (!$this->cache->contains($lookupKey)) { + return; + } + + $key = $this->cache->fetch($lookupKey); + if ($this->cache->contains($key)) { + $this->cache->delete($key); + } + + $this->cache->delete($lookupKey); + } + + /** + * {@inheritDoc} + */ + public function evictFromCacheByIdentity(ObjectIdentityInterface $oid) + { + $key = $this->getDataKeyByIdentity($oid); + if (!$this->cache->contains($key)) { + return; + } + + $this->cache->delete($key); + } + + /** + * {@inheritDoc} + */ + public function getFromCacheById($aclId) + { + $lookupKey = $this->getAliasKeyForIdentity($aclId); + if (!$this->cache->contains($lookupKey)) { + return null; + } + + $key = $this->cache->fetch($lookupKey); + if (!$this->cache->contains($key)) { + $this->cache->delete($lookupKey); + + return null; + } + + return $this->unserializeAcl($this->cache->fetch($key)); + } + + /** + * {@inheritDoc} + */ + public function getFromCacheByIdentity(ObjectIdentityInterface $oid) + { + $key = $this->getDataKeyByIdentity($oid); + if (!$this->cache->contains($key)) { + return null; + } + + return $this->unserializeAcl($this->cache->fetch($key)); + } + + /** + * {@inheritDoc} + */ + public function putInCache(AclInterface $acl) + { + if (null === $acl->getId()) { + throw new \InvalidArgumentException('Transient ACLs cannot be cached.'); + } + + if (null !== $parentAcl = $acl->getParentAcl()) { + $this->putInCache($parentAcl); + } + + $key = $this->getDataKeyByIdentity($acl->getObjectIdentity()); + $this->cache->save($key, serialize($acl)); + $this->cache->save($this->getAliasKeyForIdentity($acl->getId()), $key); + } + + /** + * Unserializes the ACL. + * + * @param string $serialized + * @return AclInterface + */ + protected function unserializeAcl($serialized) + { + $acl = unserialize($serialized); + + if (null !== $parentId = $acl->getParentAcl()) { + $parentAcl = $this->getFromCacheById($parentId); + + if (null === $parentAcl) { + return null; + } + + $acl->setParentAcl($parentAcl); + } + + $reflectionProperty = new \ReflectionProperty($acl, 'permissionGrantingStrategy'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($acl, $this->permissionGrantingStrategy); + $reflectionProperty->setAccessible(false); + + $aceAclProperty = new \ReflectionProperty('Symfony\Component\Security\Acl\Domain\Entry', 'id'); + $aceAclProperty->setAccessible(true); + + foreach ($acl->getObjectAces() as $ace) { + $aceAclProperty->setValue($ace, $acl); + } + foreach ($acl->getClassAces() as $ace) { + $aceAclProperty->setValue($ace, $acl); + } + + $aceClassFieldProperty = new \ReflectionProperty($acl, 'classFieldAces'); + $aceClassFieldProperty->setAccessible(true); + foreach ($aceClassFieldProperty->getValue($acl) as $field => $aces) { + foreach ($aces as $ace) { + $aceAclProperty->setValue($ace, $acl); + } + } + $aceClassFieldProperty->setAccessible(false); + + $aceObjectFieldProperty = new \ReflectionProperty($acl, 'objectFieldAces'); + $aceObjectFieldProperty->setAccessible(true); + foreach ($aceObjectFieldProperty->getValue($acl) as $field => $aces) { + foreach ($aces as $ace) { + $aceAclProperty->setValue($ace, $acl); + } + } + $aceObjectFieldProperty->setAccessible(false); + + $aceAclProperty->setAccessible(false); + + return $acl; + } + + /** + * Returns the key for the object identity + * + * @param ObjectIdentityInterface $oid + * @return string + */ + protected function getDataKeyByIdentity(ObjectIdentityInterface $oid) + { + return $this->prefix.md5($oid->getType()).sha1($oid->getType()) + .'_'.md5($oid->getIdentifier()).sha1($oid->getIdentifier()); + } + + /** + * Returns the alias key for the object identity key + * + * @param string $aclId + * @return string + */ + protected function getAliasKeyForIdentity($aclId) + { + return $this->prefix.$aclId; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/Entry.php b/src/Symfony/Component/Security/Acl/Domain/Entry.php new file mode 100644 index 000000000000..b6dd1f0fb4df --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/Entry.php @@ -0,0 +1,215 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Auditable ACE implementation + * + * @author Johannes M. Schmitt + */ +class Entry implements AuditableEntryInterface +{ + protected $acl; + protected $mask; + protected $id; + protected $securityIdentity; + protected $strategy; + protected $auditFailure; + protected $auditSuccess; + protected $granting; + + /** + * Constructor + * + * @param integer $id + * @param AclInterface $acl + * @param SecurityIdentityInterface $sid + * @param string $strategy + * @param integer $mask + * @param Boolean $granting + * @param Boolean $auditFailure + * @param Boolean $auditSuccess + */ + public function __construct($id, AclInterface $acl, SecurityIdentityInterface $sid, $strategy, $mask, $granting, $auditFailure, $auditSuccess) + { + $this->id = $id; + $this->acl = $acl; + $this->securityIdentity = $sid; + $this->strategy = $strategy; + $this->mask = $mask; + $this->granting = $granting; + $this->auditFailure = $auditFailure; + $this->auditSuccess = $auditSuccess; + } + + /** + * {@inheritDoc} + */ + public function getAcl() + { + return $this->acl; + } + + /** + * {@inheritDoc} + */ + public function getMask() + { + return $this->mask; + } + + /** + * {@inheritDoc} + */ + public function getId() + { + return $this->id; + } + + /** + * {@inheritDoc} + */ + public function getSecurityIdentity() + { + return $this->securityIdentity; + } + + /** + * {@inheritDoc} + */ + public function getStrategy() + { + return $this->strategy; + } + + /** + * {@inheritDoc} + */ + public function isAuditFailure() + { + return $this->auditFailure; + } + + /** + * {@inheritDoc} + */ + public function isAuditSuccess() + { + return $this->auditSuccess; + } + + /** + * {@inheritDoc} + */ + public function isGranting() + { + return $this->granting; + } + + /** + * Turns on/off auditing on permissions denials. + * + * Do never call this method directly. Use the respective methods on the + * AclInterface instead. + * + * @param Boolean $boolean + * @return void + */ + public function setAuditFailure($boolean) + { + $this->auditFailure = $boolean; + } + + /** + * Turns on/off auditing on permission grants. + * + * Do never call this method directly. Use the respective methods on the + * AclInterface instead. + * + * @param Boolean $boolean + * @return void + */ + public function setAuditSuccess($boolean) + { + $this->auditSuccess = $boolean; + } + + /** + * Sets the permission mask + * + * Do never call this method directly. Use the respective methods on the + * AclInterface instead. + * + * @param integer $mask + * @return void + */ + public function setMask($mask) + { + $this->mask = $mask; + } + + /** + * Sets the mask comparison strategy + * + * Do never call this method directly. Use the respective methods on the + * AclInterface instead. + * + * @param string $strategy + * @return void + */ + public function setStrategy($strategy) + { + $this->strategy = $strategy; + } + + /** + * Implementation of \Serializable + * + * @return string + */ + public function serialize() + { + return serialize(array( + $this->mask, + $this->id, + $this->securityIdentity, + $this->strategy, + $this->auditFailure, + $this->auditSuccess, + $this->granting, + )); + } + + /** + * Implementation of \Serializable + * + * @param string $serialized + * @return void + */ + public function unserialize($serialized) + { + list($this->mask, + $this->id, + $this->securityIdentity, + $this->strategy, + $this->auditFailure, + $this->auditSuccess, + $this->granting + ) = unserialize($serialized); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/FieldEntry.php b/src/Symfony/Component/Security/Acl/Domain/FieldEntry.php new file mode 100644 index 000000000000..0e1a40754acc --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/FieldEntry.php @@ -0,0 +1,88 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Field-aware ACE implementation which is auditable + * + * @author Johannes M. Schmitt + */ +class FieldEntry extends Entry implements FieldAwareEntryInterface +{ + protected $field; + + /** + * Constructor + * + * @param integer $id + * @param AclInterface $acl + * @param string $field + * @param SecurityIdentityInterface $sid + * @param string $strategy + * @param integer $mask + * @param Boolean $granting + * @param Boolean $auditFailure + * @param Boolean $auditSuccess + * @return void + */ + public function __construct($id, AclInterface $acl, $field, SecurityIdentityInterface $sid, $strategy, $mask, $granting, $auditFailure, $auditSuccess) + { + parent::__construct($id, $acl, $sid, $strategy, $mask, $granting, $auditFailure, $auditSuccess); + + $this->field = $field; + } + + /** + * {@inheritDoc} + */ + public function getField() + { + return $this->field; + } + + /** + * {@inheritDoc} + */ + public function serialize() + { + return serialize(array( + $this->field, + $this->mask, + $this->id, + $this->securityIdentity, + $this->strategy, + $this->auditFailure, + $this->auditSuccess, + $this->granting, + )); + } + + /** + * {@inheritDoc} + */ + public function unserialize($serialized) + { + list($this->field, + $this->mask, + $this->id, + $this->securityIdentity, + $this->strategy, + $this->auditFailure, + $this->auditSuccess, + $this->granting + ) = unserialize($serialized); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/ObjectIdentity.php b/src/Symfony/Component/Security/Acl/Domain/ObjectIdentity.php new file mode 100644 index 000000000000..37a05ebed782 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/ObjectIdentity.php @@ -0,0 +1,106 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ObjectIdentity implementation + * + * @author Johannes M. Schmitt + */ +class ObjectIdentity implements ObjectIdentityInterface +{ + protected $identifier; + protected $type; + + /** + * Constructor + * + * @param string $identifier + * @param string $type + * @return void + */ + public function __construct($identifier, $type) + { + if (0 === strlen($identifier)) { + throw new \InvalidArgumentException('$identifier cannot be empty.'); + } + if (0 === strlen($type)) { + throw new \InvalidArgumentException('$type cannot be empty.'); + } + + $this->identifier = $identifier; + $this->type = $type; + } + + /** + * Constructs an ObjectIdentity for the given domain object + * + * @param object $domainObject + * @throws \InvalidArgumentException + * @return ObjectIdentity + */ + public static function fromDomainObject($domainObject) + { + if (!is_object($domainObject)) { + throw new InvalidDomainObjectException('$domainObject must be an object.'); + } + + if ($domainObject instanceof DomainObjectInterface) { + return new self($domainObject->getObjectIdentifier(), get_class($domainObject)); + } else if (method_exists($domainObject, 'getId')) { + return new self($domainObject->getId(), get_class($domainObject)); + } + + throw new InvalidDomainObjectException('$domainObject must either implement the DomainObjectInterface, or have a method named "getId".'); + } + + /** + * {@inheritDoc} + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return $this->type; + } + + /** + * {@inheritDoc} + */ + public function equals(ObjectIdentityInterface $identity) + { + // comparing the identifier with === might lead to problems, so we + // waive this restriction + return $this->identifier == $identity->getIdentifier() + && $this->type === $identity->getType(); + } + + /** + * Returns a textual representation of this object identity + * + * @return string + */ + public function __toString() + { + return sprintf('ObjectIdentity(%s, %s)', $this->identifier, $this->type); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/ObjectIdentityRetrievalStrategy.php b/src/Symfony/Component/Security/Acl/Domain/ObjectIdentityRetrievalStrategy.php new file mode 100644 index 000000000000..64315bf08e38 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/ObjectIdentityRetrievalStrategy.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Strategy to be used for retrieving object identities from domain objects + * + * @author Johannes M. Schmitt + */ +class ObjectIdentityRetrievalStrategy implements ObjectIdentityRetrievalStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function getObjectIdentity($domainObject) + { + try { + return ObjectIdentity::fromDomainObject($domainObject); + } catch (InvalidDomainObjectException $failed) { + return null; + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/PermissionGrantingStrategy.php b/src/Symfony/Component/Security/Acl/Domain/PermissionGrantingStrategy.php new file mode 100644 index 000000000000..a349e937e631 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/PermissionGrantingStrategy.php @@ -0,0 +1,229 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * The permission granting strategy to apply to the access control list. + * + * @author Johannes M. Schmitt + */ +class PermissionGrantingStrategy implements PermissionGrantingStrategyInterface +{ + const EQUAL = 'equal'; + const ALL = 'all'; + const ANY = 'any'; + + protected $auditLogger; + + /** + * Sets the audit logger + * + * @param AuditLoggerInterface $auditLogger + * @return void + */ + public function setAuditLogger(AuditLoggerInterface $auditLogger) + { + $this->auditLogger = $auditLogger; + } + + /** + * Returns the audit logger + * + * @return AuditLoggerInterface + */ + public function getAuditLogger() + { + return $this->auditLogger; + } + + /** + * {@inheritDoc} + */ + public function isGranted(AclInterface $acl, array $masks, array $sids, $administrativeMode = false) + { + try { + try { + $aces = $acl->getObjectAces(); + + if (0 === count($aces)) { + throw new NoAceFoundException('No applicable ACE was found.'); + } + + return $this->hasSufficientPermissions($acl, $aces, $masks, $sids, $administrativeMode); + } catch (NoAceFoundException $noObjectAce) { + $aces = $acl->getClassAces(); + + if (0 === count($aces)) { + throw new NoAceFoundException('No applicable ACE was found.'); + } + + return $this->hasSufficientPermissions($acl, $aces, $masks, $sids, $administrativeMode); + } + } catch (NoAceFoundException $noClassAce) { + if ($acl->isEntriesInheriting() && null !== $parentAcl = $acl->getParentAcl()) { + return $parentAcl->isGranted($masks, $sids, $administrativeMode); + } + + throw new NoAceFoundException('No applicable ACE was found.'); + } + } + + /** + * {@inheritDoc} + */ + public function isFieldGranted(AclInterface $acl, $field, array $masks, array $sids, $administrativeMode = false) + { + try { + try { + $aces = $acl->getObjectFieldAces($field); + if (0 === count($aces)) { + throw new NoAceFoundException('No applicable ACE was found.'); + } + + return $this->hasSufficientPermissions($acl, $aces, $masks, $sids, $administrativeMode); + } catch (NoAceFoundException $noObjectAces) { + $aces = $acl->getClassFieldAces($field); + if (0 === count($aces)) { + throw new NoAceFoundException('No applicable ACE was found.'); + } + + return $this->hasSufficientPermissions($acl, $aces, $masks, $sids, $administrativeMode); + } + } catch (NoAceFoundException $noClassAces) { + if ($acl->isEntriesInheriting() && null !== $parentAcl = $acl->getParentAcl()) { + return $parentAcl->isFieldGranted($field, $masks, $sids, $administrativeMode); + } + + throw new NoAceFoundException('No applicable ACE was found.'); + } + } + + /** + * Makes an authorization decision. + * + * The order of ACEs, and SIDs is significant; the order of permission masks + * not so much. It is important to note that the more specific security + * identities should be at the beginning of the SIDs array in order for this + * strategy to produce intuitive authorization decisions. + * + * First, we will iterate over permissions, then over security identities. + * For each combination of permission, and identity we will test the + * available ACEs until we find one which is applicable. + * + * The first applicable ACE will make the ultimate decision for the + * permission/identity combination. If it is granting, this method will return + * true, if it is denying, the method will continue to check the next + * permission/identity combination. + * + * This process is repeated until either a granting ACE is found, or no + * permission/identity combinations are left. In the latter case, we will + * call this method on the parent ACL if it exists, and isEntriesInheriting + * is true. Otherwise, we will either throw an NoAceFoundException, or deny + * access finally. + * + * @param AclInterface $acl + * @param array $aces an array of ACE to check against + * @param array $masks an array of permission masks + * @param array $sids an array of SecurityIdentityInterface implementations + * @param Boolean $administrativeMode true turns off audit logging + * @return Boolean true, or false; either granting, or denying access respectively. + */ + protected function hasSufficientPermissions(AclInterface $acl, array $aces, array $masks, array $sids, $administrativeMode) + { + $firstRejectedAce = null; + + foreach ($masks as $requiredMask) { + foreach ($sids as $sid) { + if (!$acl->isSidLoaded($sid)) { + throw new SidNotLoadedException(sprintf('The SID "%s" has not been loaded.', $sid)); + } + + foreach ($aces as $ace) { + if ($this->isAceApplicable($requiredMask, $sid, $ace)) { + if ($ace->isGranting()) { + if (!$administrativeMode && null !== $this->auditLogger) { + $this->auditLogger->logIfNeeded(true, $ace); + } + + return true; + } + + if (null === $firstRejectedAce) { + $firstRejectedAce = $ace; + } + + break 2; + } + } + } + } + + if (null !== $firstRejectedAce) { + if (!$administrativeMode && null !== $this->auditLogger) { + $this->auditLogger->logIfNeeded(false, $firstRejectedAce); + } + + return false; + } + + throw new NoAceFoundException('No applicable ACE was found.'); + } + + /** + * Determines whether the ACE is applicable to the given permission/security + * identity combination. + * + * Per default, we support three different comparison strategies. + * + * Strategy ALL: + * The ACE will be considered applicable when all the turned-on bits in the + * required mask are also turned-on in the ACE mask. + * + * Strategy ANY: + * The ACE will be considered applicable when any of the turned-on bits in + * the required mask is also turned-on the in the ACE mask. + * + * Strategy EQUAL: + * The ACE will be considered applicable when the bitmasks are equal. + * + * @param SecurityIdentityInterface $sid + * @param EntryInterface $ace + * @param int $requiredMask + * @return Boolean + */ + protected function isAceApplicable($requiredMask, SecurityIdentityInterface $sid, EntryInterface $ace) + { + if (false === $ace->getSecurityIdentity()->equals($sid)) { + return false; + } + + $strategy = $ace->getStrategy(); + if (self::ALL === $strategy) { + return $requiredMask === ($ace->getMask() & $requiredMask); + } else if (self::ANY === $strategy) { + return 0 !== ($ace->getMask() & $requiredMask); + } else if (self::EQUAL === $strategy) { + return $requiredMask === $ace->getMask(); + } else { + throw new \RuntimeException(sprintf('The strategy "%s" is not supported.', $strategy)); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/RoleSecurityIdentity.php b/src/Symfony/Component/Security/Acl/Domain/RoleSecurityIdentity.php new file mode 100644 index 000000000000..4632b80b175f --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/RoleSecurityIdentity.php @@ -0,0 +1,74 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * A SecurityIdentity implementation for roles + * + * @author Johannes M. Schmitt + */ +class RoleSecurityIdentity implements SecurityIdentityInterface +{ + protected $role; + + /** + * Constructor + * + * @param mixed $role a Role instance, or its string representation + * @return void + */ + public function __construct($role) + { + if ($role instanceof Role) { + $role = $role->getRole(); + } + + $this->role = $role; + } + + /** + * Returns the role name + * + * @return string + */ + public function getRole() + { + return $this->role; + } + + /** + * {@inheritDoc} + */ + public function equals(SecurityIdentityInterface $sid) + { + if (!$sid instanceof RoleSecurityIdentity) { + return false; + } + + return $this->role === $sid->getRole(); + } + + /** + * Returns a textual representation of this security identity. + * + * This is solely used for debugging purposes, not to make an equality decision. + * + * @return string + */ + public function __toString() + { + return sprintf('RoleSecurityIdentity(%s)', $this->role); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/SecurityIdentityRetrievalStrategy.php b/src/Symfony/Component/Security/Acl/Domain/SecurityIdentityRetrievalStrategy.php new file mode 100644 index 000000000000..651233e1535b --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/SecurityIdentityRetrievalStrategy.php @@ -0,0 +1,73 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Strategy for retrieving security identities + * + * @author Johannes M. Schmitt + */ +class SecurityIdentityRetrievalStrategy implements SecurityIdentityRetrievalStrategyInterface +{ + protected $roleHierarchy; + protected $authenticationTrustResolver; + + /** + * Constructor + * + * @param RoleHierarchyInterface $roleHierarchy + * @param AuthenticationTrustResolver $authenticationTrustResolver + * @return void + */ + public function __construct(RoleHierarchyInterface $roleHierarchy, AuthenticationTrustResolver $authenticationTrustResolver) + { + $this->roleHierarchy = $roleHierarchy; + $this->authenticationTrustResolver = $authenticationTrustResolver; + } + + /** + * {@inheritDoc} + */ + public function getSecurityIdentities(TokenInterface $token) + { + $sids = array(); + + if (false === $this->authenticationTrustResolver->isAnonymous($token)) { + $sids[] = new UserSecurityIdentity($token); + } + + // add all reachable roles + foreach ($this->roleHierarchy->getReachableRoles($token->getRoles()) as $role) { + $sids[] = new RoleSecurityIdentity($role); + } + + // add built-in special roles + if ($this->authenticationTrustResolver->isFullFledged($token)) { + $sids[] = new RoleSecurityIdentity(AuthenticatedVoter::IS_AUTHENTICATED_FULLY); + $sids[] = new RoleSecurityIdentity(AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED); + $sids[] = new RoleSecurityIdentity(AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY); + } else if ($this->authenticationTrustResolver->isRememberMe($token)) { + $sids[] = new RoleSecurityIdentity(AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED); + $sids[] = new RoleSecurityIdentity(AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY); + } else if ($this->authenticationTrustResolver->isAnonymous($token)) { + $sids[] = new RoleSecurityIdentity(AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY); + } + + return $sids; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Domain/UserSecurityIdentity.php b/src/Symfony/Component/Security/Acl/Domain/UserSecurityIdentity.php new file mode 100644 index 000000000000..ddc75665613c --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Domain/UserSecurityIdentity.php @@ -0,0 +1,83 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * A SecurityIdentity implementation used for actual users + * + * FIXME: We need to also store the user provider id since the + * username might not be unique across all available user + * providers. + * + * @author Johannes M. Schmitt + */ +class UserSecurityIdentity implements SecurityIdentityInterface +{ + protected $username; + + /** + * Constructor + * + * @param mixed $username the username representation, or a TokenInterface + * implementation + * @return void + */ + public function __construct($username) + { + if ($username instanceof TokenInterface) { + $username = (string) $username; + } + + if (0 === strlen($username)) { + throw new \InvalidArgumentException('$username must not be empty.'); + } + + $this->username = $username; + } + + /** + * Returns the username + * + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * {@inheritDoc} + */ + public function equals(SecurityIdentityInterface $sid) + { + if (!$sid instanceof UserSecurityIdentity) { + return false; + } + + return $this->username === $sid->getUsername(); + } + + /** + * A textual representation of this security identity. + * + * This is not used for equality comparison, but only for debugging. + * + * @return string + */ + public function __toString() + { + return sprintf('UserSecurityIdentity(%s)', $this->username); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Exception/AclAlreadyExistsException.php b/src/Symfony/Component/Security/Acl/Exception/AclAlreadyExistsException.php new file mode 100644 index 000000000000..223b52c72d3a --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Exception/AclAlreadyExistsException.php @@ -0,0 +1,22 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This exception is thrown when someone tries to create an ACL for an object + * identity that already has one. + * + * @author Johannes M. Schmitt + */ +class AclAlreadyExistsException extends Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Exception/AclNotFoundException.php b/src/Symfony/Component/Security/Acl/Exception/AclNotFoundException.php new file mode 100644 index 000000000000..140e7399b168 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Exception/AclNotFoundException.php @@ -0,0 +1,22 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This exception is thrown when we cannot locate an ACL for a passed + * ObjectIdentity implementation. + * + * @author Johannes M. Schmitt + */ +class AclNotFoundException extends Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Exception/ConcurrentModificationException.php b/src/Symfony/Component/Security/Acl/Exception/ConcurrentModificationException.php new file mode 100644 index 000000000000..fd65c2b1c39a --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Exception/ConcurrentModificationException.php @@ -0,0 +1,13 @@ + + */ +class ConcurrentModificationException extends Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Exception/Exception.php b/src/Symfony/Component/Security/Acl/Exception/Exception.php new file mode 100644 index 000000000000..0e0add3229ec --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Exception/Exception.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Base ACL exception + * + * @author Johannes M. Schmitt + */ +class Exception extends \Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Exception/InvalidDomainObjectException.php b/src/Symfony/Component/Security/Acl/Exception/InvalidDomainObjectException.php new file mode 100644 index 000000000000..12f0b9a135a4 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Exception/InvalidDomainObjectException.php @@ -0,0 +1,13 @@ + + */ +class InvalidDomainObjectException extends Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Exception/NoAceFoundException.php b/src/Symfony/Component/Security/Acl/Exception/NoAceFoundException.php new file mode 100644 index 000000000000..788be2a26f45 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Exception/NoAceFoundException.php @@ -0,0 +1,22 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This exception is thrown when we cannot locate an ACE that matches the + * combination of permission masks and security identities. + * + * @author Johannes M. Schmitt + */ +class NoAceFoundException extends Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Exception/SidNotLoadedException.php b/src/Symfony/Component/Security/Acl/Exception/SidNotLoadedException.php new file mode 100644 index 000000000000..c856dce2682f --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Exception/SidNotLoadedException.php @@ -0,0 +1,22 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This exception is thrown when ACEs for an SID are requested which has not + * been loaded from the database. + * + * @author Johannes M. Schmitt + */ +class SidNotLoadedException extends Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/AclCacheInterface.php b/src/Symfony/Component/Security/Acl/Model/AclCacheInterface.php new file mode 100644 index 000000000000..356006f3f891 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/AclCacheInterface.php @@ -0,0 +1,69 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * AclCache Interface + * + * @author Johannes M. Schmitt + */ +interface AclCacheInterface +{ + /** + * Removes an ACL from the cache + * + * @param string $primaryKey a serialized primary key + * @return void + */ + function evictFromCacheById($primaryKey); + + /** + * Removes an ACL from the cache + * + * The ACL which is returned, must reference the passed object identity. + * + * @param ObjectIdentityInterface $oid + * @return void + */ + function evictFromCacheByIdentity(ObjectIdentityInterface $oid); + + /** + * Retrieves an ACL for the given object identity primary key from the cache + * + * @param integer $primaryKey + * @return AclInterface + */ + function getFromCacheById($primaryKey); + + /** + * Retrieves an ACL for the given object identity from the cache + * + * @param ObjectIdentityInterface $oid + * @return AclInterface + */ + function getFromCacheByIdentity(ObjectIdentityInterface $oid); + + /** + * Stores a new ACL in the cache + * + * @param AclInterface $acl + * @return void + */ + function putInCache(AclInterface $acl); + + /** + * Removes all ACLs from the cache + * + * @return void + */ + function clearCache(); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/AclInterface.php b/src/Symfony/Component/Security/Acl/Model/AclInterface.php new file mode 100644 index 000000000000..d66e8daee369 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/AclInterface.php @@ -0,0 +1,106 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This interface represents an access control list (ACL) for a domain object. + * Each domain object can have exactly one associated ACL. + * + * An ACL contains all access control entries (ACE) for a given domain object. + * In order to avoid needing references to the domain object itself, implementations + * use ObjectIdentity implementations as an additional level of indirection. + * + * @author Johannes M. Schmitt + */ +interface AclInterface extends \Serializable +{ + /** + * Returns all class-based ACEs associated with this ACL + * + * @return array + */ + function getClassAces(); + + /** + * Returns all class-field-based ACEs associated with this ACL + * + * @param string $field + * @return array + */ + function getClassFieldAces($field); + + /** + * Returns all object-based ACEs associated with this ACL + * + * @return array + */ + function getObjectAces(); + + /** + * Returns all object-field-based ACEs associated with this ACL + * + * @param string $field + * @return array + */ + function getObjectFieldAces($field); + + /** + * Returns the object identity associated with this ACL + * + * @return ObjectIdentityInterface + */ + function getObjectIdentity(); + + /** + * Returns the parent ACL, or null if there is none. + * + * @return AclInterface|null + */ + function getParentAcl(); + + /** + * Whether this ACL is inheriting ACEs from a parent ACL. + * + * @return Boolean + */ + function isEntriesInheriting(); + + /** + * Determines whether field access is granted + * + * @param string $field + * @param array $masks + * @param array $securityIdentities + * @param Boolean $administrativeMode + * @return Boolean + */ + function isFieldGranted($field, array $masks, array $securityIdentities, $administrativeMode = false); + + /** + * Determines whether access is granted + * + * @throws NoAceFoundException when no ACE was applicable for this request + * @param array $masks + * @param array $securityIdentities + * @param Boolean $administrativeMode + * @return Boolean + */ + function isGranted(array $masks, array $securityIdentities, $administrativeMode = false); + + /** + * Whether the ACL has loaded ACEs for all of the passed security identities + * + * @param mixed $securityIdentities an implementation of SecurityIdentityInterface, or an array thereof + * @return Boolean + */ + function isSidLoaded($securityIdentities); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/AclProviderInterface.php b/src/Symfony/Component/Security/Acl/Model/AclProviderInterface.php new file mode 100644 index 000000000000..238b687fc2de --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/AclProviderInterface.php @@ -0,0 +1,49 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Provides a common interface for retrieving ACLs. + * + * @author Johannes M. Schmitt + */ +interface AclProviderInterface +{ + /** + * Retrieves all child object identities from the database + * + * @param ObjectIdentityInterface $parentOid + * @param Boolean $directChildrenOnly + * @return array returns an array of child 'ObjectIdentity's + */ + function findChildren(ObjectIdentityInterface $parentOid, $directChildrenOnly = false); + + /** + * Returns the ACL that belongs to the given object identity + * + * @throws AclNotFoundException when there is no ACL + * @param ObjectIdentityInterface $oid + * @param array $sids + * @return AclInterface + */ + function findAcl(ObjectIdentityInterface $oid, array $sids = array()); + + /** + * Returns the ACLs that belong to the given object identities + * + * @throws AclNotFoundException when we cannot find an ACL for all identities + * @param array $oids an array of ObjectIdentityInterface implementations + * @param array $sids an array of SecurityIdentityInterface implementations + * @return \SplObjectStorage mapping the passed object identities to ACLs + */ + function findAcls(array $oids, array $sids = array()); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/AuditLoggerInterface.php b/src/Symfony/Component/Security/Acl/Model/AuditLoggerInterface.php new file mode 100644 index 000000000000..654085868290 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/AuditLoggerInterface.php @@ -0,0 +1,30 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Interface for audit loggers + * + * @author Johannes M. Schmitt + */ +interface AuditLoggerInterface +{ + /** + * This method is called whenever access is granted, or denied, and + * administrative mode is turned off. + * + * @param Boolean $granted + * @param EntryInterface $ace + * @return void + */ + function logIfNeeded($granted, EntryInterface $ace); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/AuditableAclInterface.php b/src/Symfony/Component/Security/Acl/Model/AuditableAclInterface.php new file mode 100644 index 000000000000..9c901d169626 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/AuditableAclInterface.php @@ -0,0 +1,63 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This interface adds auditing capabilities to the ACL. + * + * @author Johannes M. Schmitt + */ +interface AuditableAclInterface extends MutableAclInterface +{ + /** + * Updates auditing for class-based ACE + * + * @param integer $index + * @param Boolean $auditSuccess + * @param Boolean $auditFailure + * @return void + */ + function updateClassAuditing($index, $auditSuccess, $auditFailure); + + /** + * Updates auditing for class-field-based ACE + * + * @param integer $index + * @param string $field + * @param Boolean $auditSuccess + * @param Boolean $auditFailure + * @return void + */ + + function updateClassFieldAuditing($index, $field, $auditSuccess, $auditFailure); + + /** + * Updates auditing for object-based ACE + * + * @param integer $index + * @param Boolean $auditSuccess + * @param Boolean $auditFailure + * @return void + */ + function updateObjectAuditing($index, $auditSuccess, $auditFailure); + + /** + * Updates auditing for object-field-based ACE + * + * @param integer $index + * @param string $field + * @param Boolean $auditSuccess + * @param Boolean $auditFailure + * @return void + */ + function updateObjectFieldAuditing($index, $field, $auditSuccess, $auditFailure); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/AuditableEntryInterface.php b/src/Symfony/Component/Security/Acl/Model/AuditableEntryInterface.php new file mode 100644 index 000000000000..f829e8857514 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/AuditableEntryInterface.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ACEs can implement this interface if they support auditing capabilities. + * + * @author Johannes M. Schmitt + */ +interface AuditableEntryInterface extends EntryInterface +{ + /** + * Whether auditing for successful grants is turned on + * + * @return Boolean + */ + function isAuditFailure(); + + /** + * Whether auditing for successful denies is turned on + * + * @return Boolean + */ + function isAuditSuccess(); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/DomainObjectInterface.php b/src/Symfony/Component/Security/Acl/Model/DomainObjectInterface.php new file mode 100644 index 000000000000..2fa1aa6e0724 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/DomainObjectInterface.php @@ -0,0 +1,29 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This method can be implemented by domain objects which you want to store + * ACLs for if they do not have a getId() method, or getId() does not return + * a unique identifier. + * + * @author Johannes M. Schmitt + */ +interface DomainObjectInterface +{ + /** + * Returns a unique identifier for this domain object. + * + * @return string + */ + function getObjectIdentifier(); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/EntryInterface.php b/src/Symfony/Component/Security/Acl/Model/EntryInterface.php new file mode 100644 index 000000000000..476f18fe8dc4 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/EntryInterface.php @@ -0,0 +1,65 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This class represents an individual entry in the ACL list. + * + * Instances MUST be immutable, as they are returned by the ACL and should not + * allow client modification. + * + * @author Johannes M. Schmitt + */ +interface EntryInterface extends \Serializable +{ + /** + * The ACL this ACE is associated with. + * + * @return AclInterface + */ + function getAcl(); + + /** + * The primary key of this ACE + * + * @return integer + */ + function getId(); + + /** + * The permission mask of this ACE + * + * @return integer + */ + function getMask(); + + /** + * The security identity associated with this ACE + * + * @return SecurityIdentityInterface + */ + function getSecurityIdentity(); + + /** + * The strategy for comparing masks + * + * @return string + */ + function getStrategy(); + + /** + * Returns whether this ACE is granting, or denying + * + * @return Boolean + */ + function isGranting(); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/FieldAwareEntryInterface.php b/src/Symfony/Component/Security/Acl/Model/FieldAwareEntryInterface.php new file mode 100644 index 000000000000..545aa441ea9b --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/FieldAwareEntryInterface.php @@ -0,0 +1,22 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Interface for entries which are restricted to specific fields + * + * @author Johannes M. Schmitt + */ +interface FieldAwareEntryInterface +{ + function getField(); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/MutableAclInterface.php b/src/Symfony/Component/Security/Acl/Model/MutableAclInterface.php new file mode 100644 index 000000000000..305bb045e4bd --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/MutableAclInterface.php @@ -0,0 +1,174 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This interface adds mutators for the AclInterface. + * + * All changes to Access Control Entries must go through this interface. Access + * Control Entries must never be modified directly. + * + * @author Johannes M. Schmitt + */ +interface MutableAclInterface extends AclInterface, NotifyPropertyChanged +{ + /** + * Deletes a class-based ACE + * + * @param integer $index + * @return void + */ + function deleteClassAce($index); + + /** + * Deletes a class-field-based ACE + * + * @param integer $index + * @param string $field + * @return void + */ + function deleteClassFieldAce($index, $field); + + /** + * Deletes an object-based ACE + * + * @param integer $index + * @return void + */ + function deleteObjectAce($index); + + /** + * Deletes an object-field-based ACE + * + * @param integer $index + * @param string $field + * @return void + */ + function deleteObjectFieldAce($index, $field); + + /** + * Returns the primary key of this ACL + * + * @return integer + */ + function getId(); + + /** + * Inserts a class-based ACE + * + * @param SecurityIdentityInterface $sid + * @param integer $mask + * @param integer $index + * @param Boolean $granting + * @param string $strategy + * @return void + */ + function insertClassAce(SecurityIdentityInterface $sid, $mask, $index = 0, $granting = true, $strategy = null); + + /** + * Inserts a class-field-based ACE + * + * @param string $field + * @param SecurityIdentityInterface $sid + * @param integer $mask + * @param integer $index + * @param Boolean $granting + * @param string $strategy + * @return void + */ + function insertClassFieldAce($field, SecurityIdentityInterface $sid, $mask, $index = 0, $granting = true, $strategy = null); + + /** + * Inserts an object-based ACE + * + * @param SecurityIdentityInterface $sid + * @param integer $mask + * @param integer $index + * @param Boolean $granting + * @param string $strategy + * @return void + */ + function insertObjectAce(SecurityIdentityInterface $sid, $mask, $index = 0, $granting = true, $strategy = null); + + /** + * Inserts an object-field-based ACE + * + * @param string $field + * @param SecurityIdentityInterface $sid + * @param integer $mask + * @param integer $index + * @param Boolean $granting + * @param string $strategy + * @return void + */ + function insertObjectFieldAce($field, SecurityIdentityInterface $sid, $mask, $index = 0, $granting = true, $strategy = null); + + /** + * Sets whether entries are inherited + * + * @param Boolean $boolean + * @return void + */ + function setEntriesInheriting($boolean); + + /** + * Sets the parent ACL + * + * @param AclInterface $acl + * @return void + */ + function setParentAcl(AclInterface $acl); + + /** + * Updates a class-based ACE + * + * @param integer $index + * @param integer $mask + * @param string $strategy if null the strategy should not be changed + * @return void + */ + function updateClassAce($index, $mask, $strategy = null); + + /** + * Updates a class-field-based ACE + * + * @param integer $index + * @param string $field + * @param integer $mask + * @param string $strategy if null the strategy should not be changed + * @return void + */ + function updateClassFieldAce($index, $field, $mask, $strategy = null); + + /** + * Updates an object-based ACE + * + * @param integer $index + * @param integer $mask + * @param string $strategy if null the strategy should not be changed + * @return void + */ + function updateObjectAce($index, $mask, $strategy = null); + + /** + * Updates an object-field-based ACE + * + * @param integer $index + * @param string $field + * @param integer $mask + * @param string $strategy if null the strategy should not be changed + * @return void + */ + function updateObjectFieldAce($index, $field, $mask, $strategy = null); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/MutableAclProviderInterface.php b/src/Symfony/Component/Security/Acl/Model/MutableAclProviderInterface.php new file mode 100644 index 000000000000..3164af7cd10c --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/MutableAclProviderInterface.php @@ -0,0 +1,52 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Provides support for creating and storing ACL instances. + * + * @author Johannes M. Schmitt + */ +interface MutableAclProviderInterface extends AclProviderInterface +{ + /** + * Creates a new ACL for the given object identity. + * + * @throws AclAlreadyExistsException when there already is an ACL for the given + * object identity + * @param ObjectIdentityInterface $oid + * @return AclInterface + */ + function createAcl(ObjectIdentityInterface $oid); + + /** + * Deletes the ACL for a given object identity. + * + * This will automatically trigger a delete for any child ACLs. If you don't + * want child ACLs to be deleted, you will have to set their parent ACL to null. + * + * @param ObjectIdentityInterface $oid + * @return void + */ + function deleteAcl(ObjectIdentityInterface $oid); + + /** + * Persists any changes which were made to the ACL, or any associated + * access control entries. + * + * Changes to parent ACLs are not persisted. + * + * @param MutableAclInterface $acl + * @return void + */ + function updateAcl(MutableAclInterface $acl); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/ObjectIdentityInterface.php b/src/Symfony/Component/Security/Acl/Model/ObjectIdentityInterface.php new file mode 100644 index 000000000000..7f7dbc61acb9 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/ObjectIdentityInterface.php @@ -0,0 +1,49 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Represents the identity of an individual domain object instance. + * + * @author Johannes M. Schmitt + */ +interface ObjectIdentityInterface +{ + /** + * We specifically require this method so we can check for object equality + * explicitly, and do not have to rely on referencial equality instead. + * + * Though in most cases, both checks should result in the same outcome. + * + * Referential Equality: $object1 === $object2 + * Example for Object Equality: $object1->getId() === $object2->getId() + * + * @param ObjectIdentityInterface $identity + * @return Boolean + */ + function equals(ObjectIdentityInterface $identity); + + /** + * Obtains a unique identifier for this object. The identifier must not be + * re-used for other objects with the same type. + * + * @return string cannot return null + */ + function getIdentifier(); + + /** + * Returns a type for the domain object. Typically, this is the PHP class name. + * + * @return string cannot return null + */ + function getType(); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/ObjectIdentityRetrievalStrategyInterface.php b/src/Symfony/Component/Security/Acl/Model/ObjectIdentityRetrievalStrategyInterface.php new file mode 100644 index 000000000000..4709294f8b27 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/ObjectIdentityRetrievalStrategyInterface.php @@ -0,0 +1,19 @@ + + */ +interface ObjectIdentityRetrievalStrategyInterface +{ + /** + * Retrievies the object identity from a domain object + * + * @param object $domainObject + * @return ObjectIdentityInterface + */ + function getObjectIdentity($domainObject); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/PermissionGrantingStrategyInterface.php b/src/Symfony/Component/Security/Acl/Model/PermissionGrantingStrategyInterface.php new file mode 100644 index 000000000000..5b7e03ff4393 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/PermissionGrantingStrategyInterface.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Interface used by permission granting implementations. + * + * @author Johannes M. Schmitt + */ +interface PermissionGrantingStrategyInterface +{ + /** + * Determines whether access to a domain object is to be granted + * + * @param AclInterface $acl + * @param array $masks + * @param array $sids + * @param Boolean $administrativeMode + * @return Boolean + */ + function isGranted(AclInterface $acl, array $masks, array $sids, $administrativeMode = false); + + /** + * Determines whether access to a domain object's field is to be granted + * + * @param AclInterface $acl + * @param string $field + * @param array $masks + * @param array $sids + * @param Boolean $adminstrativeMode + * @return Boolean + */ + function isFieldGranted(AclInterface $acl, $field, array $masks, array $sids, $adminstrativeMode = false); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/SecurityIdentityInterface.php b/src/Symfony/Component/Security/Acl/Model/SecurityIdentityInterface.php new file mode 100644 index 000000000000..251334d190dc --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/SecurityIdentityInterface.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This interface provides an additional level of indirection, so that + * we can work with abstracted versions of security objects and do + * not have to save the entire objects. + * + * @author Johannes M. Schmitt + */ +interface SecurityIdentityInterface +{ + /** + * This method is used to compare two security identities in order to + * not rely on referential equality. + * + * @param SecurityIdentityInterface $identity + * @return void + */ + function equals(SecurityIdentityInterface $identity); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Model/SecurityIdentityRetrievalStrategyInterface.php b/src/Symfony/Component/Security/Acl/Model/SecurityIdentityRetrievalStrategyInterface.php new file mode 100644 index 000000000000..6a8bb4c80284 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Model/SecurityIdentityRetrievalStrategyInterface.php @@ -0,0 +1,25 @@ + + */ +interface SecurityIdentityRetrievalStrategyInterface +{ + /** + * Retrieves the available security identities for the given token + * + * The order in which the security identities are returned is significant. + * Typically, security identities should be ordered from most specific to + * least specific. + * + * @param TokenInterface $token + * @return array of SecurityIdentityInterface implementations + */ + function getSecurityIdentities(TokenInterface $token); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Permission/BasicPermissionMap.php b/src/Symfony/Component/Security/Acl/Permission/BasicPermissionMap.php new file mode 100644 index 000000000000..43a39d318f94 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Permission/BasicPermissionMap.php @@ -0,0 +1,103 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This is basic permission map complements the masks which have been defined + * on the standard implementation of the MaskBuilder. + * + * @author Johannes M. Schmitt + */ +class BasicPermissionMap implements PermissionMapInterface +{ + const PERMISSION_VIEW = 'VIEW'; + const PERMISSION_EDIT = 'EDIT'; + const PERMISSION_CREATE = 'CREATE'; + const PERMISSION_DELETE = 'DELETE'; + const PERMISSION_UNDELETE = 'UNDELETE'; + const PERMISSION_OPERATOR = 'OPERATOR'; + const PERMISSION_MASTER = 'MASTER'; + const PERMISSION_OWNER = 'OWNER'; + + protected $map = array( + self::PERMISSION_VIEW => array( + MaskBuilder::MASK_VIEW, + MaskBuilder::MASK_EDIT, + MaskBuilder::MASK_OPERATOR, + MaskBuilder::MASK_MASTER, + MaskBuilder::MASK_OWNER, + ), + + self::PERMISSION_EDIT => array( + MaskBuilder::MASK_EDIT, + MaskBuilder::MASK_OPERATOR, + MaskBuilder::MASK_MASTER, + MaskBuilder::MASK_OWNER, + ), + + self::PERMISSION_CREATE => array( + MaskBuilder::MASK_CREATE, + MaskBuilder::MASK_OPERATOR, + MaskBuilder::MASK_MASTER, + MaskBuilder::MASK_OWNER, + ), + + self::PERMISSION_DELETE => array( + MaskBuilder::MASK_DELETE, + MaskBuilder::MASK_OPERATOR, + MaskBuilder::MASK_MASTER, + MaskBuilder::MASK_OWNER, + ), + + self::PERMISSION_UNDELETE => array( + MaskBuilder::MASK_UNDELETE, + MaskBuilder::MASK_OPERATOR, + MaskBuilder::MASK_MASTER, + MaskBuilder::MASK_OWNER, + ), + + self::PERMISSION_OPERATOR => array( + MaskBuilder::MASK_OPERATOR, + MaskBuilder::MASK_MASTER, + MaskBuilder::MASK_OWNER, + ), + + self::PERMISSION_MASTER => array( + MaskBuilder::MASK_MASTER, + MaskBuilder::MASK_OWNER, + ), + + self::PERMISSION_OWNER => array( + MaskBuilder::MASK_OWNER, + ), + ); + + /** + * {@inheritDoc} + */ + public function getMasks($permission) + { + if (!isset($this->map[$permission])) { + throw new \InvalidArgumentException(sprintf('The permission "%s" is not supported by this implementation.', $permission)); + } + + return $this->map[$permission]; + } + + /** + * {@inheritDoc} + */ + public function contains($permission) + { + return isset($this->map[$permission]); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php b/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php new file mode 100644 index 000000000000..55aece4ab6b2 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php @@ -0,0 +1,202 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This class allows you to build cumulative permissions easily, or convert + * masks to a human-readable format. + * + * + * $builder = new MaskBuilder(); + * $builder + * ->add('view') + * ->add('create') + * ->add('edit') + * ; + * var_dump($builder->get()); // int(7) + * var_dump($builder->getPattern()); // string(32) ".............................ECV" + * + * + * We have defined some commonly used base permissions which you can use: + * - VIEW: the SID is allowed to view the domain object / field + * - CREATE: the SID is allowed to create new instances of the domain object / fields + * - EDIT: the SID is allowed to edit existing instances of the domain object / field + * - DELETE: the SID is allowed to delete domain objects + * - UNDELETE: the SID is allowed to recover domain objects from trash + * - OPERATOR: the SID is allowed to perform any action on the domain object + * except for granting others permissions + * - MASTER: the SID is allowed to perform any action on the domain object, + * and is allowed to grant other SIDs any permission except for + * MASTER and OWNER permissions + * - OWNER: the SID is owning the domain object in question and can perform any + * action on the domain object as well as grant any permission + * + * @author Johannes M. Schmitt + */ +class MaskBuilder +{ + const MASK_VIEW = 1; // 1 << 0 + const MASK_CREATE = 2; // 1 << 1 + const MASK_EDIT = 4; // 1 << 2 + const MASK_DELETE = 8; // 1 << 3 + const MASK_UNDELETE = 16; // 1 << 4 + const MASK_OPERATOR = 32; // 1 << 5 + const MASK_MASTER = 64; // 1 << 6 + const MASK_OWNER = 128; // 1 << 7 + const MASK_IDDQD = 1073741823; // 1 << 0 | 1 << 1 | ... | 1 << 30 + + const CODE_VIEW = 'V'; + const CODE_CREATE = 'C'; + const CODE_EDIT = 'E'; + const CODE_DELETE = 'D'; + const CODE_UNDELETE = 'U'; + const CODE_OPERATOR = 'O'; + const CODE_MASTER = 'M'; + const CODE_OWNER = 'N'; + + const ALL_OFF = '................................'; + const OFF = '.'; + const ON = '*'; + + protected $mask; + + /** + * Constructor + * + * @param integer $mask optional; defaults to 0 + * @return void + */ + public function __construct($mask = 0) + { + if (!is_int($mask)) { + throw new \InvalidArgumentException('$mask must be an integer.'); + } + + $this->mask = $mask; + } + + /** + * Adds a mask to the permission + * + * @param mixed $mask + * @return PermissionBuilder + */ + public function add($mask) + { + if (is_string($mask) && defined($name = 'self::MASK_'.strtoupper($mask))) { + $mask = constant($name); + } else if (!is_int($mask)) { + throw new \InvalidArgumentException('$mask must be an integer.'); + } + + $this->mask |= $mask; + + return $this; + } + + /** + * Returns the mask of this permission + * + * @return integer + */ + public function get() + { + return $this->mask; + } + + /** + * Returns a human-readable representation of the permission + * + * @return string + */ + public function getPattern() + { + $pattern = self::ALL_OFF; + $length = strlen($pattern); + $bitmask = str_pad(decbin($this->mask), $length, '0', STR_PAD_LEFT); + + for ($i=$length-1; $i>=0; $i--) { + if ('1' === $bitmask[$i]) { + try { + $pattern[$i] = self::getCode(1 << ($length - $i - 1)); + } catch (\Exception $notPredefined) { + $pattern[$i] = self::ON; + } + } + } + + return $pattern; + } + + /** + * Removes a mask from the permission + * + * @param mixed $mask + * @return PermissionBuilder + */ + public function remove($mask) + { + if (is_string($mask) && defined($name = 'self::MASK_'.strtoupper($mask))) { + $mask = constant($name); + } else if (!is_int($mask)) { + throw new \InvalidArgumentException('$mask must be an integer.'); + } + + $this->mask &= ~$mask; + + return $this; + } + + /** + * Resets the PermissionBuilder + * + * @return PermissionBuilder + */ + public function reset() + { + $this->mask = 0; + + return $this; + } + + /** + * Returns the code for the passed mask + * + * @param integer $mask + * @throws \InvalidArgumentException + * @throws \RuntimeException + * @return string + */ + public static function getCode($mask) + { + if (!is_int($mask)) { + throw new \InvalidArgumentException('$mask must be an integer.'); + } + + $reflection = new \ReflectionClass(get_called_class()); + foreach ($reflection->getConstants() as $name => $cMask) { + if (0 !== strpos($name, 'MASK_')) { + continue; + } + + if ($mask === $cMask) { + if (!defined($cName = 'self::CODE_'.substr($name, 5))) { + throw new \RuntimeException('There was no code defined for this mask.'); + } + + return constant($cName); + } + } + + throw new \InvalidArgumentException(sprintf('The mask "%d" is not supported.', $mask)); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Permission/PermissionMapInterface.php b/src/Symfony/Component/Security/Acl/Permission/PermissionMapInterface.php new file mode 100644 index 000000000000..27ee7f9ea673 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Permission/PermissionMapInterface.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This is the interface that must be implemented by permission maps. + * + * @author Johannes M. Schmitt + */ +interface PermissionMapInterface +{ + /** + * Returns an array of bitmasks. + * + * The security identity must have been granted access to at least one of + * these bitmasks. + * + * @param string $permission + * @return array + */ + function getMasks($permission); + + /** + * Whether this map contains the given permission + * + * @param string $permission + * @return Boolean + */ + function contains($permission); +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Voter/AclVoter.php b/src/Symfony/Component/Security/Acl/Voter/AclVoter.php new file mode 100644 index 000000000000..954ad9b3b92e --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Voter/AclVoter.php @@ -0,0 +1,105 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This voter can be used as a base class for implementing your own permissions. + * + * @author Johannes M. Schmitt + */ +class AclVoter implements VoterInterface +{ + protected $aclProvider; + protected $permissionMap; + protected $objectIdentityRetrievalStrategy; + protected $securityIdentityRetrievalStrategy; + + public function __construct(AclProviderInterface $aclProvider, ObjectIdentityRetrievalStrategyInterface $oidRetrievalStrategy, SecurityIdentityRetrievalStrategyInterface $sidRetrievalStrategy, PermissionMapInterface $permissionMap) + { + $this->aclProvider = $aclProvider; + $this->permissionMap = $permissionMap; + $this->objectIdentityRetrievalStrategy = $oidRetrievalStrategy; + $this->securityIdentityRetrievalStrategy = $sidRetrievalStrategy; + } + + public function supportsAttribute($attribute) + { + return $this->permissionMap->contains($attribute); + } + + public function vote(TokenInterface $token, $object, array $attributes) + { + if (null === $object) { + return self::ACCESS_ABSTAIN; + } else if ($object instanceof FieldVote) { + $field = $object->getField(); + $object = $object->getDomainObject(); + } else { + $field = null; + } + + if (null === $oid = $this->objectIdentityRetrievalStrategy->getObjectIdentity($object)) { + return self::ACCESS_ABSTAIN; + } + $sids = $this->securityIdentityRetrievalStrategy->getSecurityIdentities($token); + + foreach ($attributes as $attribute) { + if (!$this->supportsAttribute($attribute)) { + continue; + } + + try { + $acl = $this->aclProvider->findAcl($oid, $sids); + } catch (AclNotFoundException $noAcl) { + return self::ACCESS_DENIED; + } + + try { + if (null === $field && $acl->isGranted($this->permissionMap->getMasks($attribute), $sids, false)) { + return self::ACCESS_GRANTED; + } else if (null !== $field && $acl->isFieldGranted($field, $this->permissionMap->getMasks($attribute), $sids, false)) { + return self::ACCESS_GRANTED; + } else { + return self::ACCESS_DENIED; + } + } catch (NoAceFoundException $noAce) { + return self::ACCESS_DENIED; + } + } + + return self::ACCESS_ABSTAIN; + } + + /** + * You can override this method when writing a voter for a specific domain + * class. + * + * @return Boolean + */ + public function supportsClass($class) + { + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Acl/Voter/FieldVote.php b/src/Symfony/Component/Security/Acl/Voter/FieldVote.php new file mode 100644 index 000000000000..dbc4a61e425b --- /dev/null +++ b/src/Symfony/Component/Security/Acl/Voter/FieldVote.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * This class is a lightweight wrapper around field vote requests which does + * not violate any interface contracts. + * + * @author Johannes M. Schmitt + */ +class FieldVote +{ + protected $domainObject; + protected $field; + + public function __construct($domainObject, $field) + { + $this->domainObject = $domainObject; + $this->field = $field; + } + + public function getDomainObject() + { + return $this->domainObject; + } + + public function getField() + { + return $this->field; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Dbal/AclProviderBenchmarkTest.php b/tests/Symfony/Tests/Component/Security/Acl/Dbal/AclProviderBenchmarkTest.php new file mode 100644 index 000000000000..7cff1070d008 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Dbal/AclProviderBenchmarkTest.php @@ -0,0 +1,258 @@ +generateTestData(); + + // get some random test object identities from the database + $oids = array(); + $stmt = $this->con->executeQuery("SELECT object_identifier, class_type FROM acl_object_identities o INNER JOIN acl_classes c ON c.id = o.class_id ORDER BY RAND() LIMIT 25"); + foreach ($stmt->fetchAll() as $oid) { + $oids[] = new ObjectIdentity($oid['object_identifier'], $oid['class_type']); + } + + $provider = $this->getProvider(); + + $start = microtime(true); + $provider->findAcls($oids); + $time = microtime(true) - $start; + echo "Total Time: ".$time."s\n"; + } + + + protected function setUp() + { + // comment the following line, and run only this test, if you need to benchmark + $this->markTestSkipped(); + + $this->con = DriverManager::getConnection(array( + 'driver' => 'pdo_mysql', + 'host' => 'localhost', + 'user' => 'root', + 'dbname' => 'testdb', + )); + } + + protected function tearDown() + { + $this->con = null; + } + + /** + * This generates a huge amount of test data to be used mainly for benchmarking + * purposes, not so much for testing. That's why it's not called by default. + */ + protected function generateTestData() + { + $sm = $this->con->getSchemaManager(); + $sm->dropAndCreateDatabase('testdb'); + $this->con->exec("USE testdb"); + + // import the schema + $schema = new Schema($options = $this->getOptions()); + foreach ($schema->toSql($this->con->getDatabasePlatform()) as $sql) { + $this->con->exec($sql); + } + + // setup prepared statements + $this->insertClassStmt = $this->con->prepare('INSERT INTO acl_classes (id, class_type) VALUES (?, ?)'); + $this->insertSidStmt = $this->con->prepare('INSERT INTO acl_security_identities (id, identifier, username) VALUES (?, ?, ?)'); + $this->insertOidStmt = $this->con->prepare('INSERT INTO acl_object_identities (id, class_id, object_identifier, parent_object_identity_id, entries_inheriting) VALUES (?, ?, ?, ?, ?)'); + $this->insertEntryStmt = $this->con->prepare('INSERT INTO acl_entries (id, class_id, object_identity_id, field_name, ace_order, security_identity_id, mask, granting, granting_strategy, audit_success, audit_failure) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'); + $this->insertOidAncestorStmt = $this->con->prepare('INSERT INTO acl_object_identity_ancestors (object_identity_id, ancestor_id) VALUES (?, ?)'); + + for ($i=0; $i<40000; $i++) { + $this->generateAclHierarchy(); + } + } + + protected function generateAclHierarchy() + { + $rootId = $this->generateAcl($this->chooseClassId(), null, array()); + + $this->generateAclLevel(rand(1, 15), $rootId, array($rootId)); + } + + protected function generateAclLevel($depth, $parentId, $ancestors) + { + $level = count($ancestors); + for ($i=0,$t=rand(1, 10); $i<$t; $i++) { + $id = $this->generateAcl($this->chooseClassId(), $parentId, $ancestors); + + if ($level < $depth) { + $this->generateAclLevel($depth, $id, array_merge($ancestors, array($id))); + } + } + } + + protected function chooseClassId() + { + static $id = 1000; + + if ($id === 1000 || ($id < 1500 && rand(0, 1))) { + $this->insertClassStmt->execute(array($id, $this->getRandomString(rand(20, 100), 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\_'))); + $id += 1; + + return $id-1; + } + else { + return rand(1000, $id-1); + } + } + + protected function generateAcl($classId, $parentId, $ancestors) + { + static $id = 1000; + + $this->insertOidStmt->execute(array( + $id, + $classId, + $this->getRandomString(rand(20, 50)), + $parentId, + rand(0, 1), + )); + + $this->insertOidAncestorStmt->execute(array($id, $id)); + foreach ($ancestors as $ancestor) { + $this->insertOidAncestorStmt->execute(array($id, $ancestor)); + } + + $this->generateAces($classId, $id); + $id += 1; + + return $id-1; + } + + protected function chooseSid() + { + static $id = 1000; + + if ($id === 1000 || ($id < 11000 && rand(0, 1))) { + $this->insertSidStmt->execute(array( + $id, + $this->getRandomString(rand(5, 30)), + rand(0, 1) + )); + $id += 1; + + return $id-1; + } + else { + return rand(1000, $id-1); + } + } + + protected function generateAces($classId, $objectId) + { + static $id = 1000; + + $sids = array(); + $fieldOrder = array(); + + for ($i=0; $i<=30; $i++) { + $fieldName = rand(0, 1) ? null : $this->getRandomString(rand(10, 20)); + + do { + $sid = $this->chooseSid(); + } + while (array_key_exists($sid, $sids) && in_array($fieldName, $sids[$sid], true)); + + $fieldOrder[$fieldName] = array_key_exists($fieldName, $fieldOrder) ? $fieldOrder[$fieldName]+1 : 0; + if (!isset($sids[$sid])) { + $sids[$sid] = array(); + } + $sids[$sid][] = $fieldName; + + $strategy = rand(0, 2); + if ($strategy === 0) { + $strategy = PermissionGrantingStrategy::ALL; + } + else if ($strategy === 1) { + $strategy = PermissionGrantingStrategy::ANY; + } + else { + $strategy = PermissionGrantingStrategy::EQUAL; + } + + // id, cid, oid, field, order, sid, mask, granting, strategy, a success, a failure + $this->insertEntryStmt->execute(array( + $id, + $classId, + rand(0, 5) ? $objectId : null, + $fieldName, + $fieldOrder[$fieldName], + $sid, + $this->generateMask(), + rand(0, 1), + $strategy, + rand(0, 1), + rand(0, 1), + )); + + $id += 1; + } + } + + protected function generateMask() + { + $i = rand(1, 30); + $mask = 0; + + while ($i <= 30) { + $mask |= 1 << rand(0, 30); + $i++; + } + + return $mask; + } + + protected function getRandomString($length, $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') + { + $s = ''; + $cLength = strlen($chars); + + while (strlen($s) < $length) { + $s .= $chars[mt_rand(0, $cLength-1)]; + } + + return $s; + } + + protected function getOptions() + { + return 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', + ); + } + + protected function getStrategy() + { + return new PermissionGrantingStrategy(); + } + + protected function getProvider() + { + return new AclProvider($this->con, $this->getStrategy(), $this->getOptions()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Dbal/AclProviderTest.php b/tests/Symfony/Tests/Component/Security/Acl/Dbal/AclProviderTest.php new file mode 100644 index 000000000000..2461f541c7f4 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Dbal/AclProviderTest.php @@ -0,0 +1,242 @@ +getProvider()->findAcl(new ObjectIdentity('foo', 'foo')); + } + + /** + * @expectedException Symfony\Component\Security\Acl\Exception\AclNotFoundException + */ + public function testFindAclsThrowsExceptionUnlessAnACLIsFoundForEveryOID() + { + $oids = array(); + $oids[] = new ObjectIdentity('1', 'foo'); + $oids[] = new ObjectIdentity('foo', 'foo'); + + $this->getProvider()->findAcls($oids); + } + + public function testFindAcls() + { + $oids = array(); + $oids[] = new ObjectIdentity('1', 'foo'); + $oids[] = new ObjectIdentity('2', 'foo'); + + $provider = $this->getProvider(); + + $acls = $provider->findAcls($oids); + $this->assertInstanceOf('SplObjectStorage', $acls); + $this->assertEquals(2, count($acls)); + $this->assertInstanceOf('Symfony\Component\Security\Acl\Domain\Acl', $acl0 = $acls->offsetGet($oids[0])); + $this->assertInstanceOf('Symfony\Component\Security\Acl\Domain\Acl', $acl1 = $acls->offsetGet($oids[1])); + $this->assertTrue($oids[0]->equals($acl0->getObjectIdentity())); + $this->assertTrue($oids[1]->equals($acl1->getObjectIdentity())); + } + + public function testFindAclCachesAclInMemory() + { + $oid = new ObjectIdentity('1', 'foo'); + $provider = $this->getProvider(); + + $acl = $provider->findAcl($oid); + $this->assertSame($acl, $cAcl = $provider->findAcl($oid)); + + $cAces = $cAcl->getObjectAces(); + foreach ($acl->getObjectAces() as $index => $ace) { + $this->assertSame($ace, $cAces[$index]); + } + } + + public function testFindAcl() + { + $oid = new ObjectIdentity('1', 'foo'); + $provider = $this->getProvider(); + + $acl = $provider->findAcl($oid); + + $this->assertInstanceOf('Symfony\Component\Security\Acl\Domain\Acl', $acl); + $this->assertTrue($oid->equals($acl->getObjectIdentity())); + $this->assertEquals(4, $acl->getId()); + $this->assertEquals(0, count($acl->getClassAces())); + $this->assertEquals(0, count($this->getField($acl, 'classFieldAces'))); + $this->assertEquals(3, count($acl->getObjectAces())); + $this->assertEquals(0, count($this->getField($acl, 'objectFieldAces'))); + + $aces = $acl->getObjectAces(); + $this->assertInstanceOf('Symfony\Component\Security\Acl\Domain\Entry', $aces[0]); + $this->assertTrue($aces[0]->isGranting()); + $this->assertTrue($aces[0]->isAuditSuccess()); + $this->assertTrue($aces[0]->isAuditFailure()); + $this->assertEquals('all', $aces[0]->getStrategy()); + $this->assertSame(2, $aces[0]->getMask()); + + // check ACE are in correct order + $i = 0; + foreach ($aces as $index => $ace) { + $this->assertEquals($i, $index); + $i++; + } + + $sid = $aces[0]->getSecurityIdentity(); + $this->assertInstanceOf('Symfony\Component\Security\Acl\Domain\UserSecurityIdentity', $sid); + $this->assertEquals('john.doe', $sid->getUsername()); + } + + protected function setUp() + { + $this->con = DriverManager::getConnection(array( + 'driver' => 'pdo_sqlite', + 'memory' => true, + )); + + // import the schema + $schema = new Schema($options = $this->getOptions()); + foreach ($schema->toSql($this->con->getDatabasePlatform()) as $sql) { + $this->con->exec($sql); + } + + // populate the schema with some test data + $this->insertClassStmt = $this->con->prepare('INSERT INTO acl_classes (id, class_type) VALUES (?, ?)'); + foreach ($this->getClassData() as $data) { + $this->insertClassStmt->execute($data); + } + + $this->insertSidStmt = $this->con->prepare('INSERT INTO acl_security_identities (id, identifier, username) VALUES (?, ?, ?)'); + foreach ($this->getSidData() as $data) { + $this->insertSidStmt->execute($data); + } + + $this->insertOidStmt = $this->con->prepare('INSERT INTO acl_object_identities (id, class_id, object_identifier, parent_object_identity_id, entries_inheriting) VALUES (?, ?, ?, ?, ?)'); + foreach ($this->getOidData() as $data) { + $this->insertOidStmt->execute($data); + } + + $this->insertEntryStmt = $this->con->prepare('INSERT INTO acl_entries (id, class_id, object_identity_id, field_name, ace_order, security_identity_id, mask, granting, granting_strategy, audit_success, audit_failure) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'); + foreach ($this->getEntryData() as $data) { + $this->insertEntryStmt->execute($data); + } + + $this->insertOidAncestorStmt = $this->con->prepare('INSERT INTO acl_object_identity_ancestors (object_identity_id, ancestor_id) VALUES (?, ?)'); + foreach ($this->getOidAncestorData() as $data) { + $this->insertOidAncestorStmt->execute($data); + } + } + + protected function tearDown() + { + $this->con = null; + } + + protected function getField($object, $field) + { + $reflection = new \ReflectionProperty($object, $field); + $reflection->setAccessible(true); + + return $reflection->getValue($object); + } + + protected function getEntryData() + { + // id, cid, oid, field, order, sid, mask, granting, strategy, a success, a failure + return array( + array(1, 1, 1, null, 0, 1, 1, 1, 'all', 1, 1), + array(2, 1, 1, null, 1, 2, 1 << 2 | 1 << 1, 0, 'any', 0, 0), + array(3, 3, 4, null, 0, 1, 2, 1, 'all', 1, 1), + array(4, 3, 4, null, 2, 2, 1, 1, 'all', 1, 1), + array(5, 3, 4, null, 1, 3, 1, 1, 'all', 1, 1), + ); + } + + protected function getOidData() + { + // id, cid, oid, parent_oid, entries_inheriting + return array( + array(1, 1, '123', null, 1), + array(2, 2, '123', 1, 1), + array(3, 2, 'i:3:123', 1, 1), + array(4, 3, '1', 2, 1), + array(5, 3, '2', 2, 1), + ); + } + + protected function getOidAncestorData() + { + return array( + array(1, 1), + array(2, 1), + array(2, 2), + array(3, 1), + array(3, 3), + array(4, 2), + array(4, 1), + array(4, 4), + array(5, 2), + array(5, 1), + array(5, 5), + ); + } + + protected function getSidData() + { + return array( + array(1, 'john.doe', 1), + array(2, 'john.doe@foo.com', 1), + array(3, '123', 1), + array(4, 'ROLE_USER', 1), + array(5, 'ROLE_USER', 0), + array(6, 'IS_AUTHENTICATED_FULLY', 0), + ); + } + + protected function getClassData() + { + return array( + array(1, 'Bundle\SomeVendor\MyBundle\Entity\SomeEntity'), + array(2, 'Bundle\MyBundle\Entity\AnotherEntity'), + array(3, 'foo'), + ); + } + + protected function getOptions() + { + return 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', + ); + } + + protected function getStrategy() + { + return new PermissionGrantingStrategy(); + } + + protected function getProvider() + { + return new AclProvider($this->con, $this->getStrategy(), $this->getOptions()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Dbal/MutableAclProviderTest.php b/tests/Symfony/Tests/Component/Security/Acl/Dbal/MutableAclProviderTest.php new file mode 100644 index 000000000000..30ca7f556f91 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Dbal/MutableAclProviderTest.php @@ -0,0 +1,452 @@ +$getter(), $b->$getter()); + } + + self::assertTrue($a->getSecurityIdentity()->equals($b->getSecurityIdentity())); + self::assertSame($a->getAcl()->getId(), $b->getAcl()->getId()); + + if ($a instanceof AuditableEntryInterface) { + self::assertSame($a->isAuditSuccess(), $b->isAuditSuccess()); + self::assertSame($a->isAuditFailure(), $b->isAuditFailure()); + } + + if ($a instanceof FieldAwareEntryInterface) { + self::assertSame($a->getField(), $b->getField()); + } + } + + /** + * @expectedException Symfony\Component\Security\Acl\Exception\AclAlreadyExistsException + */ + public function testCreateAclThrowsExceptionWhenAclAlreadyExists() + { + $provider = $this->getProvider(); + $oid = new ObjectIdentity('123456', 'FOO'); + $provider->createAcl($oid); + $provider->createAcl($oid); + } + + public function testCreateAcl() + { + $provider = $this->getProvider(); + $oid = new ObjectIdentity('123456', 'FOO'); + $acl = $provider->createAcl($oid); + $cachedAcl = $provider->findAcl($oid); + + $this->assertInstanceOf('Symfony\Component\Security\Acl\Domain\Acl', $acl); + $this->assertSame($acl, $cachedAcl); + $this->assertTrue($acl->getObjectIdentity()->equals($oid)); + } + + public function testDeleteAcl() + { + $provider = $this->getProvider(); + $oid = new ObjectIdentity(1, 'Foo'); + $acl = $provider->createAcl($oid); + + $provider->deleteAcl($oid); + $loadedAcls = $this->getField($provider, 'loadedAcls'); + $this->assertEquals(0, count($loadedAcls['Foo'])); + + try { + $provider->findAcl($oid); + $this->fail('ACL has not been properly deleted.'); + } catch (AclNotFoundException $notFound) { } + } + + public function testDeleteAclDeletesChildren() + { + $provider = $this->getProvider(); + $acl = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + $parentAcl = $provider->createAcl(new ObjectIdentity(2, 'Foo')); + $acl->setParentAcl($parentAcl); + $provider->updateAcl($acl); + $provider->deleteAcl($parentAcl->getObjectIdentity()); + + try { + $provider->findAcl(new ObjectIdentity(1, 'Foo')); + $this->fail('Child-ACLs have not been deleted.'); + } catch (AclNotFoundException $notFound) { } + } + + public function testFindAclsAddsPropertyListener() + { + $provider = $this->getProvider(); + $acl = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + + $propertyChanges = $this->getField($provider, 'propertyChanges'); + $this->assertEquals(1, count($propertyChanges)); + $this->assertTrue($propertyChanges->contains($acl)); + $this->assertEquals(array(), $propertyChanges->offsetGet($acl)); + + $listeners = $this->getField($acl, 'listeners'); + $this->assertSame($provider, $listeners[0]); + } + + public function testFindAclsAddsPropertyListenerOnlyOnce() + { + $provider = $this->getProvider(); + $acl = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + $acl = $provider->findAcl(new ObjectIdentity(1, 'Foo')); + + $propertyChanges = $this->getField($provider, 'propertyChanges'); + $this->assertEquals(1, count($propertyChanges)); + $this->assertTrue($propertyChanges->contains($acl)); + $this->assertEquals(array(), $propertyChanges->offsetGet($acl)); + + $listeners = $this->getField($acl, 'listeners'); + $this->assertEquals(1, count($listeners)); + $this->assertSame($provider, $listeners[0]); + } + + public function testFindAclsAddsPropertyListenerToParentAcls() + { + $provider = $this->getProvider(); + $this->importAcls($provider, array( + 'main' => array( + 'object_identifier' => '1', + 'class_type' => 'foo', + 'parent_acl' => 'parent', + ), + 'parent' => array( + 'object_identifier' => '1', + 'class_type' => 'anotherFoo', + ) + )); + + $propertyChanges = $this->getField($provider, 'propertyChanges'); + $this->assertEquals(0, count($propertyChanges)); + + $acl = $provider->findAcl(new ObjectIdentity('1', 'foo')); + $this->assertEquals(2, count($propertyChanges)); + $this->assertTrue($propertyChanges->contains($acl)); + $this->assertTrue($propertyChanges->contains($acl->getParentAcl())); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testPropertyChangedDoesNotTrackUnmanagedAcls() + { + $provider = $this->getProvider(); + $acl = new Acl(1, new ObjectIdentity(1, 'foo'), new PermissionGrantingStrategy(), array(), false); + + $provider->propertyChanged($acl, 'classAces', array(), array('foo')); + } + + public function testPropertyChangedTracksChangesToAclProperties() + { + $provider = $this->getProvider(); + $acl = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + $propertyChanges = $this->getField($provider, 'propertyChanges'); + + $provider->propertyChanged($acl, 'entriesInheriting', false, true); + $changes = $propertyChanges->offsetGet($acl); + $this->assertTrue(isset($changes['entriesInheriting'])); + $this->assertFalse($changes['entriesInheriting'][0]); + $this->assertTrue($changes['entriesInheriting'][1]); + + $provider->propertyChanged($acl, 'entriesInheriting', true, false); + $provider->propertyChanged($acl, 'entriesInheriting', false, true); + $provider->propertyChanged($acl, 'entriesInheriting', true, false); + $changes = $propertyChanges->offsetGet($acl); + $this->assertFalse(isset($changes['entriesInheriting'])); + } + + public function testPropertyChangedTracksChangesToAceProperties() + { + $provider = $this->getProvider(); + $acl = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + $ace = new Entry(1, $acl, new UserSecurityIdentity('foo'), 'all', 1, true, true, true); + $ace2 = new Entry(2, $acl, new UserSecurityIdentity('foo'), 'all', 1, true, true, true); + $propertyChanges = $this->getField($provider, 'propertyChanges'); + + $provider->propertyChanged($ace, 'mask', 1, 3); + $changes = $propertyChanges->offsetGet($acl); + $this->assertTrue(isset($changes['aces'])); + $this->assertInstanceOf('\SplObjectStorage', $changes['aces']); + $this->assertTrue($changes['aces']->contains($ace)); + $aceChanges = $changes['aces']->offsetGet($ace); + $this->assertTrue(isset($aceChanges['mask'])); + $this->assertEquals(1, $aceChanges['mask'][0]); + $this->assertEquals(3, $aceChanges['mask'][1]); + + $provider->propertyChanged($ace, 'strategy', 'all', 'any'); + $changes = $propertyChanges->offsetGet($acl); + $this->assertTrue(isset($changes['aces'])); + $this->assertInstanceOf('\SplObjectStorage', $changes['aces']); + $this->assertTrue($changes['aces']->contains($ace)); + $aceChanges = $changes['aces']->offsetGet($ace); + $this->assertTrue(isset($aceChanges['mask'])); + $this->assertTrue(isset($aceChanges['strategy'])); + $this->assertEquals('all', $aceChanges['strategy'][0]); + $this->assertEquals('any', $aceChanges['strategy'][1]); + + $provider->propertyChanged($ace, 'mask', 3, 1); + $changes = $propertyChanges->offsetGet($acl); + $aceChanges = $changes['aces']->offsetGet($ace); + $this->assertFalse(isset($aceChanges['mask'])); + $this->assertTrue(isset($aceChanges['strategy'])); + + $provider->propertyChanged($ace2, 'mask', 1, 3); + $provider->propertyChanged($ace, 'strategy', 'any', 'all'); + $changes = $propertyChanges->offsetGet($acl); + $this->assertTrue(isset($changes['aces'])); + $this->assertFalse($changes['aces']->contains($ace)); + $this->assertTrue($changes['aces']->contains($ace2)); + + $provider->propertyChanged($ace2, 'mask', 3, 4); + $provider->propertyChanged($ace2, 'mask', 4, 1); + $changes = $propertyChanges->offsetGet($acl); + $this->assertFalse(isset($changes['aces'])); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testUpdateAclDoesNotAcceptUntrackedAcls() + { + $provider = $this->getProvider(); + $acl = new Acl(1, new ObjectIdentity(1, 'Foo'), new PermissionGrantingStrategy(), array(), true); + $provider->updateAcl($acl); + } + + public function testUpdateDoesNothingWhenThereAreNoChanges() + { + $con = $this->getMock('Doctrine\DBAL\Connection', array(), array(), '', false); + $con + ->expects($this->never()) + ->method('beginTransaction') + ; + $con + ->expects($this->never()) + ->method('executeQuery') + ; + + $provider = new MutableAclProvider($con, new PermissionGrantingStrategy(), array()); + $acl = new Acl(1, new ObjectIdentity(1, 'Foo'), new PermissionGrantingStrategy(), array(), true); + $propertyChanges = $this->getField($provider, 'propertyChanges'); + $propertyChanges->offsetSet($acl, array()); + $provider->updateAcl($acl); + } + + public function testUpdateAclThrowsExceptionOnConcurrentModifcationOfSharedProperties() + { + $provider = $this->getProvider(); + $acl1 = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + $acl2 = $provider->createAcl(new ObjectIdentity(2, 'Foo')); + $acl3 = $provider->createAcl(new ObjectIdentity(1, 'AnotherFoo')); + $sid = new RoleSecurityIdentity('ROLE_FOO'); + + $acl1->insertClassAce($sid, 1); + $acl3->insertClassAce($sid, 1); + $provider->updateAcl($acl1); + $provider->updateAcl($acl3); + + $acl2->insertClassAce($sid, 16); + $provider->updateAcl($acl2); + + $acl1->insertClassAce($sid, 3); + $acl2->insertClassAce($sid, 5); + try { + $provider->updateAcl($acl1); + $this->fail('Provider failed to detect a concurrent modification.'); + } catch (ConcurrentModificationException $ex) { } + } + + public function testUpdateAcl() + { + $provider = $this->getProvider(); + $acl = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + $sid = new UserSecurityIdentity('johannes'); + $acl->setEntriesInheriting(!$acl->isEntriesInheriting()); + + $acl->insertObjectAce($sid, 1); + $acl->insertClassAce($sid, 5, 0, false); + $acl->insertObjectAce($sid, 2, 1, true); + $provider->updateAcl($acl); + + $acl->updateObjectAce(0, 3); + $acl->deleteObjectAce(1); + $acl->updateObjectAuditing(0, true, false); + $provider->updateAcl($acl); + + $reloadProvider = $this->getProvider(); + $reloadedAcl = $reloadProvider->findAcl(new ObjectIdentity(1, 'Foo')); + $this->assertNotSame($acl, $reloadedAcl); + $this->assertSame($acl->isEntriesInheriting(), $reloadedAcl->isEntriesInheriting()); + + $aces = $acl->getObjectAces(); + $reloadedAces = $reloadedAcl->getObjectAces(); + $this->assertEquals(count($aces), count($reloadedAces)); + foreach ($aces as $index => $ace) { + $this->assertAceEquals($ace, $reloadedAces[$index]); + } + } + + public function testUpdateAclWorksForChangingTheParentAcl() + { + $provider = $this->getProvider(); + $acl = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + $parentAcl = $provider->createAcl(new ObjectIdentity(1, 'AnotherFoo')); + $acl->setParentAcl($parentAcl); + $provider->updateAcl($acl); + + $reloadProvider = $this->getProvider(); + $reloadedAcl = $reloadProvider->findAcl(new ObjectIdentity(1, 'Foo')); + $this->assertNotSame($acl, $reloadedAcl); + $this->assertSame($parentAcl->getId(), $reloadedAcl->getParentAcl()->getId()); + } + + /** + * Data must have the following format: + * array( + * *name* => array( + * 'object_identifier' => *required* + * 'class_type' => *required*, + * 'parent_acl' => *name (optional)* + * ), + * ) + * + * @param AclProvider $provider + * @param array $data + * @throws \InvalidArgumentException + * @throws Exception + */ + protected function importAcls(AclProvider $provider, array $data) + { + $aclIds = $parentAcls = array(); + $con = $this->getField($provider, 'connection'); + $con->beginTransaction(); + try { + foreach ($data as $name => $aclData) { + if (!isset($aclData['object_identifier'], $aclData['class_type'])) { + throw new \InvalidArgumentException('"object_identifier", and "class_type" must be present.'); + } + + $this->callMethod($provider, 'createObjectIdentity', array(new ObjectIdentity($aclData['object_identifier'], $aclData['class_type']))); + $aclId = $con->lastInsertId(); + $aclIds[$name] = $aclId; + + $sql = $this->callMethod($provider, 'getInsertObjectIdentityRelationSql', array($aclId, $aclId)); + $con->executeQuery($sql); + + if (isset($aclData['parent_acl'])) { + if (isset($aclIds[$aclData['parent_acl']])) { + $con->executeQuery("UPDATE acl_object_identities SET parent_object_identity_id = ".$aclIds[$aclData['parent_acl']]." WHERE id = ".$aclId); + $con->executeQuery($this->callMethod($provider, 'getInsertObjectIdentityRelationSql', array($aclId, $aclIds[$aclData['parent_acl']]))); + } else { + $parentAcls[$aclId] = $aclData['parent_acl']; + } + } + } + + foreach ($parentAcls as $aclId => $name) { + if (!isset($aclIds[$name])) { + throw new \InvalidArgumentException(sprintf('"%s" does not exist.', $name)); + } + + $con->executeQuery(sprintf("UPDATE acl_object_identities SET parent_object_identity_id = %d WHERE id = %d", $aclIds[$name], $aclId)); + $con->executeQuery($this->callMethod($provider, 'getInsertObjectIdentityRelationSql', array($aclId, $aclIds[$name]))); + } + + $con->commit(); + } catch (\Exception $e) { + $con->rollBack(); + + throw $e; + } + } + + protected function callMethod($object, $method, array $args) + { + $method = new \ReflectionMethod($object, $method); + $method->setAccessible(true); + + return $method->invokeArgs($object, $args); + } + + protected function setUp() + { + $this->con = DriverManager::getConnection(array( + 'driver' => 'pdo_sqlite', + 'memory' => true, + )); + + // import the schema + $schema = new Schema($this->getOptions()); + foreach ($schema->toSql($this->con->getDatabasePlatform()) as $sql) { + $this->con->exec($sql); + } + } + + protected function tearDown() + { + $this->con = null; + } + + protected function getField($object, $field) + { + $reflection = new \ReflectionProperty($object, $field); + $reflection->setAccessible(true); + + return $reflection->getValue($object); + } + + public function setField($object, $field, $value) + { + $reflection = new \ReflectionProperty($object, $field); + $reflection->setAccessible(true); + $reflection->setValue($object, $value); + $reflection->setAccessible(false); + } + + protected function getOptions() + { + return 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', + ); + } + + protected function getStrategy() + { + return new PermissionGrantingStrategy(); + } + + protected function getProvider($cache = null) + { + return new MutableAclProvider($this->con, $this->getStrategy(), $this->getOptions(), $cache); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/AclTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/AclTest.php new file mode 100644 index 000000000000..81ec3c780f99 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/AclTest.php @@ -0,0 +1,502 @@ +assertSame(1, $acl->getId()); + $this->assertSame($oid, $acl->getObjectIdentity()); + $this->assertNull($acl->getParentAcl()); + $this->assertTrue($acl->isEntriesInheriting()); + } + + /** + * @expectedException \OutOfBoundsException + * @dataProvider getDeleteAceTests + */ + public function testDeleteAceThrowsExceptionOnInvalidIndex($type) + { + $acl = $this->getAcl(); + $acl->{'delete'.$type.'Ace'}(0); + } + + /** + * @dataProvider getDeleteAceTests + */ + public function testDeleteAce($type) + { + $acl = $this->getAcl(); + $acl->{'insert'.$type.'Ace'}(new RoleSecurityIdentity('foo'), 1); + $acl->{'insert'.$type.'Ace'}(new RoleSecurityIdentity('foo'), 2, 1); + $acl->{'insert'.$type.'Ace'}(new RoleSecurityIdentity('foo'), 3, 2); + + $listener = $this->getListener(array( + $type.'Aces', 'aceOrder', 'aceOrder', $type.'Aces', + )); + $acl->addPropertyChangedListener($listener); + + $this->assertEquals(3, count($acl->{'get'.$type.'Aces'}())); + + $acl->{'delete'.$type.'Ace'}(0); + $this->assertEquals(2, count($aces = $acl->{'get'.$type.'Aces'}())); + $this->assertEquals(2, $aces[0]->getMask()); + $this->assertEquals(3, $aces[1]->getMask()); + + $acl->{'delete'.$type.'Ace'}(1); + $this->assertEquals(1, count($aces = $acl->{'get'.$type.'Aces'}())); + $this->assertEquals(2, $aces[0]->getMask()); + } + + public function getDeleteAceTests() + { + return array( + array('class'), + array('object'), + ); + } + + /** + * @expectedException \OutOfBoundsException + * @dataProvider getDeleteFieldAceTests + */ + public function testDeleteFieldAceThrowsExceptionOnInvalidIndex($type) + { + $acl = $this->getAcl(); + $acl->{'delete'.$type.'Ace'}('foo', 0); + } + + /** + * @dataProvider getDeleteFieldAceTests + */ + public function testDeleteFieldAce($type) + { + $acl = $this->getAcl(); + $acl->{'insert'.$type.'Ace'}('foo', new RoleSecurityIdentity('foo'), 1, 0); + $acl->{'insert'.$type.'Ace'}('foo', new RoleSecurityIdentity('foo'), 2, 1); + $acl->{'insert'.$type.'Ace'}('foo', new RoleSecurityIdentity('foo'), 3, 2); + + $listener = $this->getListener(array( + $type.'Aces', 'aceOrder', 'aceOrder', $type.'Aces', + )); + $acl->addPropertyChangedListener($listener); + + $this->assertEquals(3, count($acl->{'get'.$type.'Aces'}('foo'))); + + $acl->{'delete'.$type.'Ace'}(0, 'foo'); + $this->assertEquals(2, count($aces = $acl->{'get'.$type.'Aces'}('foo'))); + $this->assertEquals(2, $aces[0]->getMask()); + $this->assertEquals(3, $aces[1]->getMask()); + + $acl->{'delete'.$type.'Ace'}(1, 'foo'); + $this->assertEquals(1, count($aces = $acl->{'get'.$type.'Aces'}('foo'))); + $this->assertEquals(2, $aces[0]->getMask()); + } + + public function getDeleteFieldAceTests() + { + return array( + array('classField'), + array('objectField'), + ); + } + + /** + * @dataProvider getInsertAceTests + */ + public function testInsertAce($property, $method) + { + $acl = $this->getAcl(); + + $listener = $this->getListener(array( + $property, 'aceOrder', $property, 'aceOrder', $property + )); + $acl->addPropertyChangedListener($listener); + + $sid = new RoleSecurityIdentity('foo'); + $acl->$method($sid, 1); + $acl->$method($sid, 2); + $acl->$method($sid, 3, 1, false); + + $this->assertEquals(3, count($aces = $acl->{'get'.$property}())); + $this->assertEquals(2, $aces[0]->getMask()); + $this->assertEquals(3, $aces[1]->getMask()); + $this->assertEquals(1, $aces[2]->getMask()); + } + + /** + * @expectedException \OutOfBoundsException + * @dataProvider getInsertAceTests + */ + public function testInsertClassAceThrowsExceptionOnInvalidIndex($property, $method) + { + $acl = $this->getAcl(); + $acl->$method(new RoleSecurityIdentity('foo'), 1, 1); + } + + public function getInsertAceTests() + { + return array( + array('classAces', 'insertClassAce'), + array('objectAces', 'insertObjectAce'), + ); + } + + /** + * @dataProvider getInsertFieldAceTests + */ + public function testInsertClassFieldAce($property, $method) + { + $acl = $this->getAcl(); + + $listener = $this->getListener(array( + $property, $property, 'aceOrder', $property, + 'aceOrder', 'aceOrder', $property, + )); + $acl->addPropertyChangedListener($listener); + + $sid = new RoleSecurityIdentity('foo'); + $acl->$method('foo', $sid, 1); + $acl->$method('foo2', $sid, 1); + $acl->$method('foo', $sid, 3); + $acl->$method('foo', $sid, 2); + + $this->assertEquals(3, count($aces = $acl->{'get'.$property}('foo'))); + $this->assertEquals(1, count($acl->{'get'.$property}('foo2'))); + $this->assertEquals(2, $aces[0]->getMask()); + $this->assertEquals(3, $aces[1]->getMask()); + $this->assertEquals(1, $aces[2]->getMask()); + } + + /** + * @expectedException \OutOfBoundsException + * @dataProvider getInsertFieldAceTests + */ + public function testInsertClassFieldAceThrowsExceptionOnInvalidIndex($property, $method) + { + $acl = $this->getAcl(); + $acl->$method('foo', new RoleSecurityIdentity('foo'), 1, 1); + } + + public function getInsertFieldAceTests() + { + return array( + array('classFieldAces', 'insertClassFieldAce'), + array('objectFieldAces', 'insertObjectFieldAce'), + ); + } + + public function testIsFieldGranted() + { + $sids = array(new RoleSecurityIdentity('ROLE_FOO'), new RoleSecurityIdentity('ROLE_IDDQD')); + $masks = array(1, 2, 4); + $strategy = $this->getMock('Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface'); + $acl = new Acl(1, new ObjectIdentity(1, 'foo'), $strategy, array(), true); + + $strategy + ->expects($this->once()) + ->method('isFieldGranted') + ->with($this->equalTo($acl), $this->equalTo('foo'), $this->equalTo($masks), $this->equalTo($sids), $this->isTrue()) + ->will($this->returnValue(true)) + ; + + $this->assertTrue($acl->isFieldGranted('foo', $masks, $sids, true)); + } + + public function testIsGranted() + { + $sids = array(new RoleSecurityIdentity('ROLE_FOO'), new RoleSecurityIdentity('ROLE_IDDQD')); + $masks = array(1, 2, 4); + $strategy = $this->getMock('Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface'); + $acl = new Acl(1, new ObjectIdentity(1, 'foo'), $strategy, array(), true); + + $strategy + ->expects($this->once()) + ->method('isGranted') + ->with($this->equalTo($acl), $this->equalTo($masks), $this->equalTo($sids), $this->isTrue()) + ->will($this->returnValue(true)) + ; + + $this->assertTrue($acl->isGranted($masks, $sids, true)); + } + + public function testSetGetParentAcl() + { + $acl = $this->getAcl(); + $parentAcl = $this->getAcl(); + + $listener = $this->getListener(array('parentAcl')); + $acl->addPropertyChangedListener($listener); + + $this->assertNull($acl->getParentAcl()); + $acl->setParentAcl($parentAcl); + $this->assertSame($parentAcl, $acl->getParentAcl()); + } + + public function testSetIsEntriesInheriting() + { + $acl = $this->getAcl(); + + $listener = $this->getListener(array('entriesInheriting')); + $acl->addPropertyChangedListener($listener); + + $this->assertTrue($acl->isEntriesInheriting()); + $acl->setEntriesInheriting(false); + $this->assertFalse($acl->isEntriesInheriting()); + } + + public function testIsSidLoadedWhenAllSidsAreLoaded() + { + $acl = $this->getAcl(); + + $this->assertTrue($acl->isSidLoaded(new UserSecurityIdentity('foo'))); + $this->assertTrue($acl->isSidLoaded(new RoleSecurityIdentity('ROLE_FOO'))); + } + + public function testIsSidLoaded() + { + $acl = new Acl(1, new ObjectIdentity('1', 'foo'), new PermissionGrantingStrategy(), array(new UserSecurityIdentity('foo'), new UserSecurityIdentity('johannes')), true); + + $this->assertTrue($acl->isSidLoaded(new UserSecurityIdentity('foo'))); + $this->assertTrue($acl->isSidLoaded(new UserSecurityIdentity('johannes'))); + $this->assertTrue($acl->isSidLoaded(array( + new UserSecurityIdentity('foo'), + new UserSecurityIdentity('johannes'), + ))); + $this->assertFalse($acl->isSidLoaded(new RoleSecurityIdentity('ROLE_FOO'))); + $this->assertFalse($acl->isSidLoaded(new UserSecurityIdentity('schmittjoh@gmail.com'))); + $this->assertFalse($acl->isSidLoaded(array( + new UserSecurityIdentity('foo'), + new UserSecurityIdentity('johannes'), + new RoleSecurityIdentity('ROLE_FOO'), + ))); + } + + /** + * @dataProvider getUpdateAceTests + * @expectedException \OutOfBoundsException + */ + public function testUpdateAceThrowsOutOfBoundsExceptionOnInvalidIndex($type) + { + $acl = $this->getAcl(); + $acl->{'update'.$type}(0, 1); + } + + /** + * @dataProvider getUpdateAceTests + */ + public function testUpdateAce($type) + { + $acl = $this->getAcl(); + $acl->{'insert'.$type}(new RoleSecurityIdentity('foo'), 1); + + $listener = $this->getListener(array( + 'mask', 'mask', 'strategy', + )); + $acl->addPropertyChangedListener($listener); + + $aces = $acl->{'get'.$type.'s'}(); + $ace = reset($aces); + $this->assertEquals(1, $ace->getMask()); + $this->assertEquals('all', $ace->getStrategy()); + + $acl->{'update'.$type}(0, 3); + $this->assertEquals(3, $ace->getMask()); + $this->assertEquals('all', $ace->getStrategy()); + + $acl->{'update'.$type}(0, 1, 'foo'); + $this->assertEquals(1, $ace->getMask()); + $this->assertEquals('foo', $ace->getStrategy()); + } + + public function getUpdateAceTests() + { + return array( + array('classAce'), + array('objectAce'), + ); + } + + /** + * @dataProvider getUpdateFieldAceTests + * @expectedException \OutOfBoundsException + */ + public function testUpdateFieldAceThrowsExceptionOnInvalidIndex($type) + { + $acl = $this->getAcl(); + $acl->{'update'.$type}(0, 'foo', 1); + } + + /** + * @dataProvider getUpdateFieldAceTests + */ + public function testUpdateFieldAce($type) + { + $acl = $this->getAcl(); + $acl->{'insert'.$type}('foo', new UserSecurityIdentity('foo'), 1); + + $listener = $this->getListener(array( + 'mask', 'mask', 'strategy' + )); + $acl->addPropertyChangedListener($listener); + + $aces = $acl->{'get'.$type.'s'}('foo'); + $ace = reset($aces); + $this->assertEquals(1, $ace->getMask()); + $this->assertEquals('all', $ace->getStrategy()); + + $acl->{'update'.$type}(0, 'foo', 3); + $this->assertEquals(3, $ace->getMask()); + $this->assertEquals('all', $ace->getStrategy()); + + $acl->{'update'.$type}(0, 'foo', 1, 'foo'); + $this->assertEquals(1, $ace->getMask()); + $this->assertEquals('foo', $ace->getStrategy()); + } + + public function getUpdateFieldAceTests() + { + return array( + array('classFieldAce'), + array('objectFieldAce'), + ); + } + + /** + * @dataProvider getUpdateAuditingTests + * @expectedException \OutOfBoundsException + */ + public function testUpdateAuditingThrowsExceptionOnInvalidIndex($type) + { + $acl = $this->getAcl(); + $acl->{'update'.$type.'Auditing'}(0, true, false); + } + + /** + * @dataProvider getUpdateAuditingTests + */ + public function testUpdateAuditing($type) + { + $acl = $this->getAcl(); + $acl->{'insert'.$type.'Ace'}(new RoleSecurityIdentity('foo'), 1); + + $listener = $this->getListener(array( + 'auditFailure', 'auditSuccess', 'auditFailure', + )); + $acl->addPropertyChangedListener($listener); + + $aces = $acl->{'get'.$type.'Aces'}(); + $ace = reset($aces); + $this->assertFalse($ace->isAuditSuccess()); + $this->assertFalse($ace->isAuditFailure()); + + $acl->{'update'.$type.'Auditing'}(0, false, true); + $this->assertFalse($ace->isAuditSuccess()); + $this->assertTrue($ace->isAuditFailure()); + + $acl->{'update'.$type.'Auditing'}(0, true, false); + $this->assertTrue($ace->isAuditSuccess()); + $this->assertFalse($ace->isAuditFailure()); + } + + public function getUpdateAuditingTests() + { + return array( + array('class'), + array('object'), + ); + } + + /** + * @expectedException \InvalidArgumentException + * @dataProvider getUpdateFieldAuditingTests + */ + public function testUpdateFieldAuditingthrowsExceptionOnInvalidField($type) + { + $acl = $this->getAcl(); + $acl->{'update'.$type.'Auditing'}(0, 'foo', true, true); + } + + /** + * @expectedException \OutOfBoundsException + * @dataProvider getUpdateFieldAuditingTests + */ + public function testUpdateFieldAuditingThrowsExceptionOnInvalidIndex($type) + { + $acl = $this->getAcl(); + $acl->{'insert'.$type.'Ace'}('foo', new RoleSecurityIdentity('foo'), 1); + $acl->{'update'.$type.'Auditing'}(1, 'foo', true, false); + } + + /** + * @dataProvider getUpdateFieldAuditingTests + */ + public function testUpdateFieldAuditing($type) + { + $acl = $this->getAcl(); + $acl->{'insert'.$type.'Ace'}('foo', new RoleSecurityIdentity('foo'), 1); + + $listener = $this->getListener(array( + 'auditSuccess', 'auditSuccess', 'auditFailure', + )); + $acl->addPropertyChangedListener($listener); + + $aces = $acl->{'get'.$type.'Aces'}('foo'); + $ace = reset($aces); + $this->assertFalse($ace->isAuditSuccess()); + $this->assertFalse($ace->isAuditFailure()); + + $acl->{'update'.$type.'Auditing'}(0, 'foo', true, false); + $this->assertTrue($ace->isAuditSuccess()); + $this->assertFalse($ace->isAuditFailure()); + + $acl->{'update'.$type.'Auditing'}(0, 'foo', false, true); + $this->assertFalse($ace->isAuditSuccess()); + $this->assertTrue($ace->isAuditFailure()); + } + + public function getUpdateFieldAuditingTests() + { + return array( + array('classField'), + array('objectField'), + ); + } + + protected function getListener($expectedChanges) + { + $aceProperties = array('aceOrder', 'mask', 'strategy', 'auditSuccess', 'auditFailure'); + + $listener = $this->getMock('Doctrine\Common\PropertyChangedListener'); + foreach ($expectedChanges as $index => $property) { + if (in_array($property, $aceProperties)) { + $class = 'Symfony\Component\Security\Acl\Domain\Entry'; + } else { + $class = 'Symfony\Component\Security\Acl\Domain\Acl'; + } + + $listener + ->expects($this->at($index)) + ->method('propertyChanged') + ->with($this->isInstanceOf($class), $this->equalTo($property)) + ; + } + + return $listener; + } + + protected function getAcl() + { + return new Acl(1, new ObjectIdentity(1, 'foo'), new PermissionGrantingStrategy(), array(), true); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/AuditLoggerTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/AuditLoggerTest.php new file mode 100644 index 000000000000..4aac56c3bf99 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/AuditLoggerTest.php @@ -0,0 +1,76 @@ +getLogger(); + $ace = $this->getEntry(); + + if (true === $granting) { + $ace + ->expects($this->once()) + ->method('isAuditSuccess') + ->will($this->returnValue($audit)) + ; + + $ace + ->expects($this->never()) + ->method('isAuditFailure') + ; + } + else { + $ace + ->expects($this->never()) + ->method('isAuditSuccess') + ; + + $ace + ->expects($this->once()) + ->method('isAuditFailure') + ->will($this->returnValue($audit)) + ; + } + + if (true === $audit) { + $logger + ->expects($this->once()) + ->method('doLog') + ->with($this->equalTo($granting), $this->equalTo($ace)) + ; + } + else { + $logger + ->expects($this->never()) + ->method('doLog') + ; + } + + $logger->logIfNeeded($granting, $ace); + } + + public function getTestLogData() + { + return array( + array(true, false), + array(true, true), + array(false, false), + array(false, true), + ); + } + + protected function getEntry() + { + return $this->getMock('Symfony\Component\Security\Acl\Model\AuditableEntryInterface'); + } + + protected function getLogger() + { + return $this->getMockForAbstractClass('Symfony\Component\Security\Acl\Domain\AuditLogger'); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/DoctrineAclCacheTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/DoctrineAclCacheTest.php new file mode 100644 index 000000000000..581fe923edfe --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/DoctrineAclCacheTest.php @@ -0,0 +1,94 @@ +getPermissionGrantingStrategy(), $empty); + } + + public function getEmptyValue() + { + return array( + array(null), + array(false), + array(''), + ); + } + + public function test() + { + $cache = $this->getCache(); + + $aclWithParent = $this->getAcl(1); + $acl = $this->getAcl(); + + $cache->putInCache($aclWithParent); + $cache->putInCache($acl); + + $cachedAcl = $cache->getFromCacheByIdentity($acl->getObjectIdentity()); + $this->assertEquals($acl->getId(), $cachedAcl->getId()); + $this->assertNull($acl->getParentAcl()); + + $cachedAclWithParent = $cache->getFromCacheByIdentity($aclWithParent->getObjectIdentity()); + $this->assertEquals($aclWithParent->getId(), $cachedAclWithParent->getId()); + $this->assertNotNull($cachedParentAcl = $cachedAclWithParent->getParentAcl()); + $this->assertEquals($aclWithParent->getParentAcl()->getId(), $cachedParentAcl->getId()); + } + + protected function getAcl($depth = 0) + { + static $id = 1; + + $acl = new Acl($id, new ObjectIdentity($id, 'foo'), $this->getPermissionGrantingStrategy(), array(), $depth > 0); + + // insert some ACEs + $sid = new UserSecurityIdentity('johannes'); + $acl->insertClassAce($sid, 1); + $acl->insertClassFieldAce('foo', $sid, 1); + $acl->insertObjectAce($sid, 1); + $acl->insertObjectFieldAce('foo', $sid, 1); + $id++; + + if ($depth > 0) { + $acl->setParentAcl($this->getAcl($depth - 1)); + } + + return $acl; + } + + protected function getPermissionGrantingStrategy() + { + if (null === $this->permissionGrantingStrategy) { + $this->permissionGrantingStrategy = new PermissionGrantingStrategy(); + } + + return $this->permissionGrantingStrategy; + } + + protected function getCache($cacheDriver = null, $prefix = DoctrineAclCache::PREFIX) + { + if (null === $cacheDriver) { + $cacheDriver = new ArrayCache(); + } + + return new DoctrineAclCache($cacheDriver, $this->getPermissionGrantingStrategy(), $prefix); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/EntryTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/EntryTest.php new file mode 100644 index 000000000000..8b0c9c2519d2 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/EntryTest.php @@ -0,0 +1,110 @@ +getAce($acl = $this->getAcl(), $sid = $this->getSid()); + + $this->assertEquals(123, $ace->getId()); + $this->assertSame($acl, $ace->getAcl()); + $this->assertSame($sid, $ace->getSecurityIdentity()); + $this->assertEquals('foostrat', $ace->getStrategy()); + $this->assertEquals(123456, $ace->getMask()); + $this->assertTrue($ace->isGranting()); + $this->assertTrue($ace->isAuditSuccess()); + $this->assertFalse($ace->isAuditFailure()); + } + + public function testSetAuditSuccess() + { + $ace = $this->getAce(); + + $this->assertTrue($ace->isAuditSuccess()); + $ace->setAuditSuccess(false); + $this->assertFalse($ace->isAuditSuccess()); + $ace->setAuditsuccess(true); + $this->assertTrue($ace->isAuditSuccess()); + } + + public function testSetAuditFailure() + { + $ace = $this->getAce(); + + $this->assertFalse($ace->isAuditFailure()); + $ace->setAuditFailure(true); + $this->assertTrue($ace->isAuditFailure()); + $ace->setAuditFailure(false); + $this->assertFalse($ace->isAuditFailure()); + } + + public function testSetMask() + { + $ace = $this->getAce(); + + $this->assertEquals(123456, $ace->getMask()); + $ace->setMask(4321); + $this->assertEquals(4321, $ace->getMask()); + } + + public function testSetStrategy() + { + $ace = $this->getAce(); + + $this->assertEquals('foostrat', $ace->getStrategy()); + $ace->setStrategy('foo'); + $this->assertEquals('foo', $ace->getStrategy()); + } + + public function testSerializeUnserialize() + { + $ace = $this->getAce(); + + $serialized = serialize($ace); + $uAce = unserialize($serialized); + + $this->assertNull($uAce->getAcl()); + $this->assertInstanceOf('Symfony\Component\Security\Acl\Model\SecurityIdentityInterface', $uAce->getSecurityIdentity()); + $this->assertEquals($ace->getId(), $uAce->getId()); + $this->assertEquals($ace->getMask(), $uAce->getMask()); + $this->assertEquals($ace->getStrategy(), $uAce->getStrategy()); + $this->assertEquals($ace->isGranting(), $uAce->isGranting()); + $this->assertEquals($ace->isAuditSuccess(), $uAce->isAuditSuccess()); + $this->assertEquals($ace->isAuditFailure(), $uAce->isAuditFailure()); + } + + protected function getAce($acl = null, $sid = null) + { + if (null === $acl) { + $acl = $this->getAcl(); + } + if (null === $sid) { + $sid = $this->getSid(); + } + + return new Entry( + 123, + $acl, + $sid, + 'foostrat', + 123456, + true, + false, + true + ); + } + + protected function getAcl() + { + return $this->getMock('Symfony\Component\Security\Acl\Model\AclInterface'); + } + + protected function getSid() + { + return $this->getMock('Symfony\Component\Security\Acl\Model\SecurityIdentityInterface'); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/FieldEntryTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/FieldEntryTest.php new file mode 100644 index 000000000000..cef3567c9947 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/FieldEntryTest.php @@ -0,0 +1,65 @@ +getAce(); + + $this->assertEquals('foo', $ace->getField()); + } + + public function testSerializeUnserialize() + { + $ace = $this->getAce(); + + $serialized = serialize($ace); + $uAce = unserialize($serialized); + + $this->assertNull($uAce->getAcl()); + $this->assertInstanceOf('Symfony\Component\Security\Acl\Model\SecurityIdentityInterface', $uAce->getSecurityIdentity()); + $this->assertEquals($ace->getId(), $uAce->getId()); + $this->assertEquals($ace->getField(), $uAce->getField()); + $this->assertEquals($ace->getMask(), $uAce->getMask()); + $this->assertEquals($ace->getStrategy(), $uAce->getStrategy()); + $this->assertEquals($ace->isGranting(), $uAce->isGranting()); + $this->assertEquals($ace->isAuditSuccess(), $uAce->isAuditSuccess()); + $this->assertEquals($ace->isAuditFailure(), $uAce->isAuditFailure()); + } + + protected function getAce($acl = null, $sid = null) + { + if (null === $acl) { + $acl = $this->getAcl(); + } + if (null === $sid) { + $sid = $this->getSid(); + } + + return new FieldEntry( + 123, + $acl, + 'foo', + $sid, + 'foostrat', + 123456, + true, + false, + true + ); + } + + protected function getAcl() + { + return $this->getMock('Symfony\Component\Security\Acl\Model\AclInterface'); + } + + protected function getSid() + { + return $this->getMock('Symfony\Component\Security\Acl\Model\SecurityIdentityInterface'); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/ObjectIdentityRetrievalStrategyTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/ObjectIdentityRetrievalStrategyTest.php new file mode 100644 index 000000000000..b799022547ac --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/ObjectIdentityRetrievalStrategyTest.php @@ -0,0 +1,34 @@ +assertNull($strategy->getObjectIdentity('foo')); + } + + public function testGetObjectIdentity() + { + $strategy = new ObjectIdentityRetrievalStrategy(); + $domainObject = new DomainObject(); + $objectIdentity = $strategy->getObjectIdentity($domainObject); + + $this->assertEquals($domainObject->getId(), $objectIdentity->getIdentifier()); + $this->assertEquals(get_class($domainObject), $objectIdentity->getType()); + } +} + +class DomainObject +{ + public function getId() + { + return 'foo'; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/ObjectIdentityTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/ObjectIdentityTest.php new file mode 100644 index 000000000000..3502077ab2d7 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/ObjectIdentityTest.php @@ -0,0 +1,76 @@ +assertEquals('fooid', $id->getIdentifier()); + $this->assertEquals('footype', $id->getType()); + } + + public function testFromDomainObjectPrefersInterfaceOverGetId() + { + $domainObject = $this->getMock('Symfony\Component\Security\Acl\Model\DomainObjectInterface'); + $domainObject + ->expects($this->once()) + ->method('getObjectIdentifier') + ->will($this->returnValue('getObjectIdentifier()')) + ; + $domainObject + ->expects($this->never()) + ->method('getId') + ->will($this->returnValue('getId()')) + ; + + $id = ObjectIdentity::fromDomainObject($domainObject); + $this->assertEquals('getObjectIdentifier()', $id->getIdentifier()); + } + + public function testFromDomainObjectWithoutInterface() + { + $id = ObjectIdentity::fromDomainObject(new TestDomainObject()); + $this->assertEquals('getId()', $id->getIdentifier()); + } + + /** + * @dataProvider getCompareData + */ + public function testEquals($oid1, $oid2, $equal) + { + if ($equal) { + $this->assertTrue($oid1->equals($oid2)); + } + else { + $this->assertFalse($oid1->equals($oid2)); + } + } + + public function getCompareData() + { + return array( + array(new ObjectIdentity('123', 'foo'), new ObjectIdentity('123', 'foo'), true), + array(new ObjectIdentity('123', 'foo'), new ObjectIdentity(123, 'foo'), true), + array(new ObjectIdentity('1', 'foo'), new ObjectIdentity('2', 'foo'), false), + array(new ObjectIdentity('1', 'bla'), new ObjectIdentity('1', 'blub'), false), + ); + } +} + +class TestDomainObject +{ + public function getObjectIdentifier() + { + return 'getObjectIdentifier()'; + } + + public function getId() + { + return 'getId()'; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/PermissionGrantingStrategyTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/PermissionGrantingStrategyTest.php new file mode 100644 index 000000000000..b43d01e6ae74 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/PermissionGrantingStrategyTest.php @@ -0,0 +1,190 @@ +getMock('Symfony\Component\Security\Acl\Model\AuditLoggerInterface'); + + $this->assertNull($strategy->getAuditLogger()); + $strategy->setAuditLogger($logger); + $this->assertSame($logger, $strategy->getAuditLogger()); + } + + public function testIsGrantedObjectAcesHavePriority() + { + $strategy = new PermissionGrantingStrategy(); + $acl = $this->getAcl($strategy); + $sid = new UserSecurityIdentity('johannes'); + + $acl->insertClassAce($sid, 1); + $acl->insertObjectAce($sid, 1, 0, false); + $this->assertFalse($strategy->isGranted($acl, array(1), array($sid))); + } + + public function testIsGrantedFallsbackToClassAcesIfNoApplicableObjectAceWasFound() + { + $strategy = new PermissionGrantingStrategy(); + $acl = $this->getAcl($strategy); + $sid = new UserSecurityIdentity('johannes'); + + $acl->insertClassAce($sid, 1); + $this->assertTrue($strategy->isGranted($acl, array(1), array($sid))); + } + + public function testIsGrantedFavorsLocalAcesOverParentAclAces() + { + $strategy = new PermissionGrantingStrategy(); + $sid = new UserSecurityIdentity('johannes'); + + $acl = $this->getAcl($strategy); + $acl->insertClassAce($sid, 1); + + $parentAcl = $this->getAcl($strategy); + $acl->setParentAcl($parentAcl); + $parentAcl->insertClassAce($sid, 1, 0, false); + + $this->assertTrue($strategy->isGranted($acl, array(1), array($sid))); + } + + public function testIsGrantedFallsBackToParentAcesIfNoLocalAcesAreApplicable() + { + $strategy = new PermissionGrantingStrategy(); + $sid = new UserSecurityIdentity('johannes'); + $anotherSid = new UserSecurityIdentity('ROLE_USER'); + + $acl = $this->getAcl($strategy); + $acl->insertClassAce($anotherSid, 1, 0, false); + + $parentAcl = $this->getAcl($strategy); + $acl->setParentAcl($parentAcl); + $parentAcl->insertClassAce($sid, 1); + + $this->assertTrue($strategy->isGranted($acl, array(1), array($sid))); + } + + /** + * @expectedException Symfony\Component\Security\Acl\Exception\NoAceFoundException + */ + public function testIsGrantedReturnsExceptionIfNoAceIsFound() + { + $strategy = new PermissionGrantingStrategy(); + $acl = $this->getAcl($strategy); + $sid = new UserSecurityIdentity('johannes'); + + $strategy->isGranted($acl, array(1), array($sid)); + } + + public function testIsGrantedFirstApplicableEntryMakesUltimateDecisionForPermissionIdentityCombination() + { + $strategy = new PermissionGrantingStrategy(); + $acl = $this->getAcl($strategy); + $sid = new UserSecurityIdentity('johannes'); + $aSid = new RoleSecurityIdentity('ROLE_USER'); + + $acl->insertClassAce($aSid, 1); + $acl->insertClassAce($sid, 1, 1, false); + $acl->insertClassAce($sid, 1, 2); + $this->assertFalse($strategy->isGranted($acl, array(1), array($sid, $aSid))); + + $acl->insertObjectAce($sid, 1, 0, false); + $acl->insertObjectAce($aSid, 1, 1); + $this->assertFalse($strategy->isGranted($acl, array(1), array($sid, $aSid))); + } + + public function testIsGrantedCallsAuditLoggerOnGrant() + { + $strategy = new PermissionGrantingStrategy(); + $acl = $this->getAcl($strategy); + $sid = new UserSecurityIdentity('johannes'); + + $logger = $this->getMock('Symfony\Component\Security\Acl\Model\AuditLoggerInterface'); + $logger + ->expects($this->once()) + ->method('logIfNeeded') + ; + $strategy->setAuditLogger($logger); + + $acl->insertObjectAce($sid, 1); + $acl->updateObjectAuditing(0, true, false); + + $this->assertTrue($strategy->isGranted($acl, array(1), array($sid))); + } + + public function testIsGrantedCallsAuditLoggerOnDeny() + { + $strategy = new PermissionGrantingStrategy(); + $acl = $this->getAcl($strategy); + $sid = new UserSecurityIdentity('johannes'); + + $logger = $this->getMock('Symfony\Component\Security\Acl\Model\AuditLoggerInterface'); + $logger + ->expects($this->once()) + ->method('logIfNeeded') + ; + $strategy->setAuditLogger($logger); + + $acl->insertObjectAce($sid, 1, 0, false); + $acl->updateObjectAuditing(0, false, true); + + $this->assertFalse($strategy->isGranted($acl, array(1), array($sid))); + } + + /** + * @dataProvider getAllStrategyTests + */ + public function testIsGrantedStrategies($maskStrategy, $aceMask, $requiredMask, $result) + { + $strategy = new PermissionGrantingStrategy(); + $acl = $this->getAcl($strategy); + $sid = new UserSecurityIdentity('johannes'); + + $acl->insertObjectAce($sid, $aceMask, 0, true, $maskStrategy); + + if (false === $result) { + try { + $strategy->isGranted($acl, array($requiredMask), array($sid)); + $this->fail('The ACE is not supposed to match.'); + } catch (NoAceFoundException $noAce) { } + } else { + $this->assertTrue($strategy->isGranted($acl, array($requiredMask), array($sid))); + } + } + + public function getAllStrategyTests() + { + return array( + array('all', 1 << 0 | 1 << 1, 1 << 0, true), + array('all', 1 << 0 | 1 << 1, 1 << 2, false), + array('all', 1 << 0 | 1 << 10, 1 << 0 | 1 << 10, true), + array('all', 1 << 0 | 1 << 1, 1 << 0 | 1 << 1 || 1 << 2, false), + array('any', 1 << 0 | 1 << 1, 1 << 0, true), + array('any', 1 << 0 | 1 << 1, 1 << 0 | 1 << 2, true), + array('any', 1 << 0 | 1 << 1, 1 << 2, false), + array('equal', 1 << 0 | 1 << 1, 1 << 0, false), + array('equal', 1 << 0 | 1 << 1, 1 << 1, false), + array('equal', 1 << 0 | 1 << 1, 1 << 0 | 1 << 1, true), + ); + } + + protected function getAcl($strategy) + { + static $id = 1; + return new Acl($id++, new ObjectIdentity(1, 'Foo'), $strategy, array(), true); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/RoleSecurityIdentityTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/RoleSecurityIdentityTest.php new file mode 100644 index 000000000000..d6fb9005ce7d --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/RoleSecurityIdentityTest.php @@ -0,0 +1,49 @@ +assertEquals('ROLE_FOO', $id->getRole()); + } + + public function testConstructorWithRoleInstance() + { + $id = new RoleSecurityIdentity(new Role('ROLE_FOO')); + + $this->assertEquals('ROLE_FOO', $id->getRole()); + } + + /** + * @dataProvider getCompareData + */ + public function testEquals($id1, $id2, $equal) + { + if ($equal) { + $this->assertTrue($id1->equals($id2)); + } + else { + $this->assertFalse($id1->equals($id2)); + } + } + + public function getCompareData() + { + return array( + array(new RoleSecurityIdentity('ROLE_FOO'), new RoleSecurityIdentity('ROLE_FOO'), true), + array(new RoleSecurityIdentity('ROLE_FOO'), new RoleSecurityIdentity(new Role('ROLE_FOO')), true), + array(new RoleSecurityIdentity('ROLE_USER'), new RoleSecurityIdentity('ROLE_FOO'), false), + array(new RoleSecurityIdentity('ROLE_FOO'), new UserSecurityIdentity('ROLE_FOO'), false), + ); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/SecurityIdentityRetrievalStrategyTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/SecurityIdentityRetrievalStrategyTest.php new file mode 100644 index 000000000000..10dada81121e --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/SecurityIdentityRetrievalStrategyTest.php @@ -0,0 +1,131 @@ +getStrategy($roles, $authenticationStatus); + $token = $this->getMock('Symfony\Component\Security\Authentication\Token\TokenInterface'); + + if ('anonymous' !== $authenticationStatus) { + $token + ->expects($this->once()) + ->method('__toString') + ->will($this->returnValue($username)) + ; + } + $token + ->expects($this->once()) + ->method('getRoles') + ->will($this->returnValue(array('foo'))) + ; + + $extractedSids = $strategy->getSecurityIdentities($token); + + foreach ($extractedSids as $index => $extractedSid) { + if (!isset($sids[$index])) { + $this->fail(sprintf('Expected SID at index %d, but there was none.', true)); + } + + if (false === $sids[$index]->equals($extractedSid)) { + $this->fail(sprintf('Index: %d, expected SID "%s", but got "%s".', $index, $sids[$index], $extractedSid)); + } + } + } + + public function getSecurityIdentityRetrievalTests() + { + return array( + array('johannes', array('ROLE_USER', 'ROLE_SUPERADMIN'), 'fullFledged', array( + new UserSecurityIdentity('johannes'), + new RoleSecurityIdentity('ROLE_USER'), + new RoleSecurityIdentity('ROLE_SUPERADMIN'), + new RoleSecurityIdentity('IS_AUTHENTICATED_FULLY'), + new RoleSecurityIdentity('IS_AUTHENTICATED_REMEMBERED'), + new RoleSecurityIdentity('IS_AUTHENTICATED_ANONYMOUSLY'), + )), + array('foo', array('ROLE_FOO'), 'rememberMe', array( + new UserSecurityIdentity('foo'), + new RoleSecurityIdentity('ROLE_FOO'), + new RoleSecurityIdentity('IS_AUTHENTICATED_REMEMBERED'), + new RoleSecurityIdentity('IS_AUTHENTICATED_ANONYMOUSLY'), + )), + array('guest', array('ROLE_FOO'), 'anonymous', array( + new RoleSecurityIdentity('ROLE_FOO'), + new RoleSecurityIdentity('IS_AUTHENTICATED_ANONYMOUSLY'), + )) + ); + } + + protected function getStrategy(array $roles = array(), $authenticationStatus = 'fullFledged') + { + $roleHierarchy = $this->getMock('Symfony\Component\Security\Role\RoleHierarchyInterface'); + $roleHierarchy + ->expects($this->once()) + ->method('getReachableRoles') + ->with($this->equalTo(array('foo'))) + ->will($this->returnValue($roles)) + ; + + $trustResolver = $this->getMock('Symfony\Component\Security\Authentication\AuthenticationTrustResolver', array(), array('', '')); + + $trustResolver + ->expects($this->at(0)) + ->method('isAnonymous') + ->will($this->returnValue('anonymous' === $authenticationStatus)) + ; + + if ('fullFledged' === $authenticationStatus) { + $trustResolver + ->expects($this->once()) + ->method('isFullFledged') + ->will($this->returnValue(true)) + ; + $trustResolver + ->expects($this->never()) + ->method('isRememberMe') + ; + } else if ('rememberMe' === $authenticationStatus) { + $trustResolver + ->expects($this->once()) + ->method('isFullFledged') + ->will($this->returnValue(false)) + ; + $trustResolver + ->expects($this->once()) + ->method('isRememberMe') + ->will($this->returnValue(true)) + ; + } else { + $trustResolver + ->expects($this->at(1)) + ->method('isAnonymous') + ->will($this->returnValue(true)) + ; + $trustResolver + ->expects($this->once()) + ->method('isFullFledged') + ->will($this->returnValue(false)) + ; + $trustResolver + ->expects($this->once()) + ->method('isRememberMe') + ->will($this->returnValue(false)) + ; + } + + + return new SecurityIdentityRetrievalStrategy($roleHierarchy, $trustResolver); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Domain/UserSecurityIdentityTest.php b/tests/Symfony/Tests/Component/Security/Acl/Domain/UserSecurityIdentityTest.php new file mode 100644 index 000000000000..2ccb890fe996 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Domain/UserSecurityIdentityTest.php @@ -0,0 +1,61 @@ +assertEquals('foo', $id->getUsername()); + } + + public function testConstructorWithToken() + { + $token = $this->getMock('Symfony\Component\Security\Authentication\Token\TokenInterface'); + $token + ->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('foo')) + ; + + $id = new UserSecurityIdentity($token); + + $this->assertEquals('foo', $id->getUsername()); + } + + /** + * @dataProvider getCompareData + */ + public function testEquals($id1, $id2, $equal) + { + if ($equal) { + $this->assertTrue($id1->equals($id2)); + } + else { + $this->assertFalse($id1->equals($id2)); + } + } + + public function getCompareData() + { + $token = $this->getMock('Symfony\Component\Security\Authentication\Token\TokenInterface'); + $token + ->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('foo')) + ; + + return array( + array(new UserSecurityIdentity('foo'), new UserSecurityIdentity('foo'), true), + array(new UserSecurityIdentity('foo'), new UserSecurityIdentity($token), true), + array(new UserSecurityIdentity('bla'), new UserSecurityIdentity('blub'), false), + array(new UserSecurityIdentity('foo'), new RoleSecurityIdentity('foo'), false), + ); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Acl/Permission/MaskBuilderTest.php b/tests/Symfony/Tests/Component/Security/Acl/Permission/MaskBuilderTest.php new file mode 100644 index 000000000000..09dbce27eaa1 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Acl/Permission/MaskBuilderTest.php @@ -0,0 +1,94 @@ +assertEquals(0, $builder->get()); + } + + public function testConstructor() + { + $builder = new MaskBuilder(123456); + + $this->assertEquals(123456, $builder->get()); + } + + public function testAddAndRemove() + { + $builder = new MaskBuilder(); + + $builder + ->add('view') + ->add('eDiT') + ->add('ownEr') + ; + $mask = $builder->get(); + + $this->assertEquals(MaskBuilder::MASK_VIEW, $mask & MaskBuilder::MASK_VIEW); + $this->assertEquals(MaskBuilder::MASK_EDIT, $mask & MaskBuilder::MASK_EDIT); + $this->assertEquals(MaskBuilder::MASK_OWNER, $mask & MaskBuilder::MASK_OWNER); + $this->assertEquals(0, $mask & MaskBuilder::MASK_MASTER); + $this->assertEquals(0, $mask & MaskBuilder::MASK_CREATE); + $this->assertEquals(0, $mask & MaskBuilder::MASK_DELETE); + $this->assertEquals(0, $mask & MaskBuilder::MASK_UNDELETE); + + $builder->remove('edit')->remove('OWner'); + $mask = $builder->get(); + $this->assertEquals(0, $mask & MaskBuilder::MASK_EDIT); + $this->assertEquals(0, $mask & MaskBuilder::MASK_OWNER); + $this->assertEquals(MaskBuilder::MASK_VIEW, $mask & MaskBuilder::MASK_VIEW); + } + + public function testGetPattern() + { + $builder = new MaskBuilder; + $this->assertEquals(MaskBuilder::ALL_OFF, $builder->getPattern()); + + $builder->add('view'); + $this->assertEquals(str_repeat('.', 31).'V', $builder->getPattern()); + + $builder->add('owner'); + $this->assertEquals(str_repeat('.', 24).'N......V', $builder->getPattern()); + + $builder->add(1 << 10); + $this->assertEquals(str_repeat('.', 21).MaskBuilder::ON.'..N......V', $builder->getPattern()); + } + + public function testReset() + { + $builder = new MaskBuilder(); + $this->assertEquals(0, $builder->get()); + + $builder->add('view'); + $this->assertTrue($builder->get() > 0); + + $builder->reset(); + $this->assertEquals(0, $builder->get()); + } +} \ No newline at end of file