diff --git a/src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml b/src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml
index 78a3bc20c7..18e393332e 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 0000000000..e1448a1974
--- /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 0000000000..3bcab49e6a
--- /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 930cd4d927..2f72b55542 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 @@ class SecurityExtension extends Extension
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 0000000000..ddb26295c6
--- /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 6f61806003..527809b68e 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 @@ class SecurityHelper extends Helper
$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 0b0b2d9409..970ea59d48 100644
--- a/src/Symfony/Bundle/TwigBundle/Extension/SecurityExtension.php
+++ b/src/Symfony/Bundle/TwigBundle/Extension/SecurityExtension.php
@@ -27,11 +27,19 @@ class SecurityExtension extends \Twig_Extension
$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 0000000000..3664b0c52c
--- /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 0000000000..6da3ec8bb1
--- /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 0000000000..16959444ef
--- /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 0000000000..c0c9830714
--- /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 0000000000..12faa4c2be
--- /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 0000000000..c6ad999ada
--- /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 0000000000..b6dd1f0fb4
--- /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 0000000000..0e1a40754a
--- /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 0000000000..37a05ebed7
--- /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 0000000000..64315bf08e
--- /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 0000000000..a349e937e6
--- /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 0000000000..4632b80b17
--- /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 0000000000..651233e153
--- /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 0000000000..ddc7566561
--- /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 0000000000..223b52c72d
--- /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 0000000000..140e7399b1
--- /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 0000000000..fd65c2b1c3
--- /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 0000000000..0e0add3229
--- /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 0000000000..12f0b9a135
--- /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 0000000000..788be2a26f
--- /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 0000000000..c856dce268
--- /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 0000000000..356006f3f8
--- /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 0000000000..d66e8daee3
--- /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 0000000000..238b687fc2
--- /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 0000000000..6540858682
--- /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 0000000000..9c901d1696
--- /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 0000000000..f829e88575
--- /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 0000000000..2fa1aa6e07
--- /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 0000000000..476f18fe8d
--- /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 0000000000..545aa441ea
--- /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 0000000000..305bb045e4
--- /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 0000000000..3164af7cd1
--- /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 0000000000..7f7dbc61ac
--- /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 0000000000..4709294f8b
--- /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 0000000000..5b7e03ff43
--- /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 0000000000..251334d190
--- /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 0000000000..6a8bb4c802
--- /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 0000000000..43a39d318f
--- /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 0000000000..55aece4ab6
--- /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 0000000000..27ee7f9ea6
--- /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 0000000000..954ad9b3b9
--- /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 0000000000..dbc4a61e42
--- /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 0000000000..7cff1070d0
--- /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 0000000000..2461f541c7
--- /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 0000000000..30ca7f556f
--- /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 0000000000..81ec3c780f
--- /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 0000000000..4aac56c3bf
--- /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 0000000000..581fe923ed
--- /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 0000000000..8b0c9c2519
--- /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 0000000000..cef3567c99
--- /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 0000000000..b799022547
--- /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 0000000000..3502077ab2
--- /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 0000000000..b43d01e6ae
--- /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 0000000000..d6fb9005ce
--- /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 0000000000..10dada8112
--- /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 0000000000..2ccb890fe9
--- /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 0000000000..09dbce27ea
--- /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