summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Acl/Dbal/AclProvider.php624
-rw-r--r--Acl/Dbal/MutableAclProvider.php887
-rw-r--r--Acl/Dbal/Schema.php145
-rw-r--r--Acl/Domain/Acl.php679
-rw-r--r--Acl/Domain/AuditLogger.php53
-rw-r--r--Acl/Domain/DoctrineAclCache.php222
-rw-r--r--Acl/Domain/Entry.php215
-rw-r--r--Acl/Domain/FieldEntry.php88
-rw-r--r--Acl/Domain/ObjectIdentity.php106
-rw-r--r--Acl/Domain/ObjectIdentityRetrievalStrategy.php35
-rw-r--r--Acl/Domain/PermissionGrantingStrategy.php229
-rw-r--r--Acl/Domain/RoleSecurityIdentity.php74
-rw-r--r--Acl/Domain/SecurityIdentityRetrievalStrategy.php73
-rw-r--r--Acl/Domain/UserSecurityIdentity.php83
-rw-r--r--Acl/Exception/AclAlreadyExistsException.php22
-rw-r--r--Acl/Exception/AclNotFoundException.php22
-rw-r--r--Acl/Exception/ConcurrentModificationException.php13
-rw-r--r--Acl/Exception/Exception.php21
-rw-r--r--Acl/Exception/InvalidDomainObjectException.php13
-rw-r--r--Acl/Exception/NoAceFoundException.php22
-rw-r--r--Acl/Exception/SidNotLoadedException.php22
-rw-r--r--Acl/Model/AclCacheInterface.php69
-rw-r--r--Acl/Model/AclInterface.php106
-rw-r--r--Acl/Model/AclProviderInterface.php49
-rw-r--r--Acl/Model/AuditLoggerInterface.php30
-rw-r--r--Acl/Model/AuditableAclInterface.php63
-rw-r--r--Acl/Model/AuditableEntryInterface.php34
-rw-r--r--Acl/Model/DomainObjectInterface.php29
-rw-r--r--Acl/Model/EntryInterface.php65
-rw-r--r--Acl/Model/FieldAwareEntryInterface.php22
-rw-r--r--Acl/Model/MutableAclInterface.php174
-rw-r--r--Acl/Model/MutableAclProviderInterface.php52
-rw-r--r--Acl/Model/ObjectIdentityInterface.php49
-rw-r--r--Acl/Model/ObjectIdentityRetrievalStrategyInterface.php19
-rw-r--r--Acl/Model/PermissionGrantingStrategyInterface.php43
-rw-r--r--Acl/Model/SecurityIdentityInterface.php31
-rw-r--r--Acl/Model/SecurityIdentityRetrievalStrategyInterface.php25
-rw-r--r--Acl/Permission/BasicPermissionMap.php103
-rw-r--r--Acl/Permission/MaskBuilder.php202
-rw-r--r--Acl/Permission/PermissionMapInterface.php39
-rw-r--r--Acl/Voter/AclVoter.php105
-rw-r--r--Acl/Voter/FieldVote.php40
42 files changed, 4997 insertions, 0 deletions
diff --git a/Acl/Dbal/AclProvider.php b/Acl/Dbal/AclProvider.php
new file mode 100644
index 0000000..3664b0c
--- /dev/null
+++ b/Acl/Dbal/AclProvider.php
@@ -0,0 +1,624 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Dbal;
+
+use Doctrine\DBAL\Driver\Connection;
+use Doctrine\DBAL\Driver\Statement;
+use Symfony\Component\Security\Acl\Domain\Acl;
+use Symfony\Component\Security\Acl\Domain\Entry;
+use Symfony\Component\Security\Acl\Domain\FieldEntry;
+use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
+use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
+use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
+use Symfony\Component\Security\Acl\Exception\AclNotFoundException;
+use Symfony\Component\Security\Acl\Model\AclCacheInterface;
+use Symfony\Component\Security\Acl\Model\AclProviderInterface;
+use Symfony\Component\Security\Acl\Model\ObjectIdentityInterface;
+use Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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 = <<<SELECTCLAUSE
+ SELECT
+ o.id as acl_id,
+ o.object_identifier,
+ o.parent_object_identity_id,
+ o.entries_inheriting,
+ c.class_type,
+ e.id as ace_id,
+ e.object_identity_id,
+ e.field_name,
+ e.ace_order,
+ e.mask,
+ e.granting,
+ e.granting_strategy,
+ e.audit_success,
+ e.audit_failure,
+ s.username,
+ s.identifier as security_identifier
+ FROM
+ {$this->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 = <<<SELECTCLAUSE
+ SELECT a.ancestor_id
+ FROM acl_object_identities o
+ INNER JOIN acl_classes c ON c.id = o.class_id
+ INNER JOIN acl_object_identity_ancestors a ON a.object_identity_id = o.id
+ WHERE (
+SELECTCLAUSE;
+
+ $where = '(o.object_identifier = %s AND c.class_type = %s)';
+ for ($i=0,$c=count($batch); $i<$c; $i++) {
+ $sql .= sprintf(
+ $where,
+ $this->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 = <<<FINDCHILDREN
+ SELECT o.object_identifier, c.class_type
+ FROM
+ {$this->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 = <<<FINDCHILDREN
+ SELECT o.object_identifier, c.class_type
+ FROM {$this->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 = <<<QUERY
+ SELECT o.id
+ FROM %s o
+ INNER JOIN %s c ON c.id = o.class_id
+ WHERE o.object_identifier = %s AND c.class_type = %s
+ LIMIT 1
+QUERY;
+
+ return sprintf(
+ $query,
+ $this->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/Acl/Dbal/MutableAclProvider.php b/Acl/Dbal/MutableAclProvider.php
new file mode 100644
index 0000000..6da3ec8
--- /dev/null
+++ b/Acl/Dbal/MutableAclProvider.php
@@ -0,0 +1,887 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Dbal;
+
+use Doctrine\Common\PropertyChangedListener;
+use Doctrine\DBAL\Driver\Connection;
+use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
+use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
+use Symfony\Component\Security\Acl\Exception\AclAlreadyExistsException;
+use Symfony\Component\Security\Acl\Exception\ConcurrentModificationException;
+use Symfony\Component\Security\Acl\Exception\Exception;
+use Symfony\Component\Security\Acl\Model\AclCacheInterface;
+use Symfony\Component\Security\Acl\Model\AclInterface;
+use Symfony\Component\Security\Acl\Model\EntryInterface;
+use Symfony\Component\Security\Acl\Model\MutableAclInterface;
+use Symfony\Component\Security\Acl\Model\MutableAclProviderInterface;
+use Symfony\Component\Security\Acl\Model\ObjectIdentityInterface;
+use Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface;
+use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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 = <<<QUERY
+ INSERT INTO %s (
+ class_id,
+ object_identity_id,
+ field_name,
+ ace_order,
+ security_identity_id,
+ mask,
+ granting,
+ granting_strategy,
+ audit_success,
+ audit_failure
+ )
+ VALUES (%d, %s, %s, %d, %d, %d, %s, %s, %s, %s)
+QUERY;
+
+ return sprintf(
+ $query,
+ $this->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 = <<<QUERY
+ INSERT INTO %s (class_id, object_identifier, entries_inheriting)
+ VALUES (%d, %s, %s)
+QUERY;
+
+ return sprintf(
+ $query,
+ $this->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/Acl/Dbal/Schema.php b/Acl/Dbal/Schema.php
new file mode 100644
index 0000000..1695944
--- /dev/null
+++ b/Acl/Dbal/Schema.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Dbal;
+
+use Doctrine\DBAL\Schema\Schema as BaseSchema;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/Acl.php b/Acl/Domain/Acl.php
new file mode 100644
index 0000000..c0c9830
--- /dev/null
+++ b/Acl/Domain/Acl.php
@@ -0,0 +1,679 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Doctrine\Common\PropertyChangedListener;
+use Symfony\Component\Security\Acl\Model\AclInterface;
+use Symfony\Component\Security\Acl\Model\AuditableAclInterface;
+use Symfony\Component\Security\Acl\Model\EntryInterface;
+use Symfony\Component\Security\Acl\Model\MutableAclInterface;
+use Symfony\Component\Security\Acl\Model\ObjectIdentityInterface;
+use Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface;
+use Symfony\Component\Security\Acl\Model\PermissionInterface;
+use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/AuditLogger.php b/Acl/Domain/AuditLogger.php
new file mode 100644
index 0000000..12faa4c
--- /dev/null
+++ b/Acl/Domain/AuditLogger.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Acl\Model\AuditableEntryInterface;
+use Symfony\Component\Security\Acl\Model\EntryInterface;
+use Symfony\Component\Security\Acl\Model\AuditLoggerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/DoctrineAclCache.php b/Acl/Domain/DoctrineAclCache.php
new file mode 100644
index 0000000..c6ad999
--- /dev/null
+++ b/Acl/Domain/DoctrineAclCache.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Doctrine\Common\Cache\Cache;
+use Symfony\Component\Security\Acl\Model\AclCacheInterface;
+use Symfony\Component\Security\Acl\Model\AclInterface;
+use Symfony\Component\Security\Acl\Model\ObjectIdentityInterface;
+use Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/Entry.php b/Acl/Domain/Entry.php
new file mode 100644
index 0000000..b6dd1f0
--- /dev/null
+++ b/Acl/Domain/Entry.php
@@ -0,0 +1,215 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Acl\Model\AclInterface;
+use Symfony\Component\Security\Acl\Model\AuditableEntryInterface;
+use Symfony\Component\Security\Acl\Model\EntryInterface;
+use Symfony\Component\Security\Acl\Model\PermissionInterface;
+use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/FieldEntry.php b/Acl/Domain/FieldEntry.php
new file mode 100644
index 0000000..0e1a407
--- /dev/null
+++ b/Acl/Domain/FieldEntry.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Acl\Model\AclInterface;
+use Symfony\Component\Security\Acl\Model\FieldAwareEntryInterface;
+use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/ObjectIdentity.php b/Acl/Domain/ObjectIdentity.php
new file mode 100644
index 0000000..37a05eb
--- /dev/null
+++ b/Acl/Domain/ObjectIdentity.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Acl\Exception\InvalidDomainObjectException;
+use Symfony\Component\Security\Acl\Model\DomainObjectInterface;
+use Symfony\Component\Security\Acl\Model\ObjectIdentityInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/ObjectIdentityRetrievalStrategy.php b/Acl/Domain/ObjectIdentityRetrievalStrategy.php
new file mode 100644
index 0000000..64315bf
--- /dev/null
+++ b/Acl/Domain/ObjectIdentityRetrievalStrategy.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Acl\Exception\InvalidDomainObjectException;
+use Symfony\Component\Security\Acl\Model\ObjectIdentityRetrievalStrategyInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/PermissionGrantingStrategy.php b/Acl/Domain/PermissionGrantingStrategy.php
new file mode 100644
index 0000000..a349e93
--- /dev/null
+++ b/Acl/Domain/PermissionGrantingStrategy.php
@@ -0,0 +1,229 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Acl\Exception\NoAceFoundException;
+use Symfony\Component\Security\Acl\Exception\SidNotLoadedException;
+use Symfony\Component\Security\Acl\Model\AclInterface;
+use Symfony\Component\Security\Acl\Model\AuditLoggerInterface;
+use Symfony\Component\Security\Acl\Model\EntryInterface;
+use Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface;
+use Symfony\Component\Security\Acl\Model\PermissionInterface;
+use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/RoleSecurityIdentity.php b/Acl/Domain/RoleSecurityIdentity.php
new file mode 100644
index 0000000..4632b80
--- /dev/null
+++ b/Acl/Domain/RoleSecurityIdentity.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface;
+use Symfony\Component\Security\Role\Role;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/SecurityIdentityRetrievalStrategy.php b/Acl/Domain/SecurityIdentityRetrievalStrategy.php
new file mode 100644
index 0000000..651233e
--- /dev/null
+++ b/Acl/Domain/SecurityIdentityRetrievalStrategy.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Acl\Model\SecurityIdentityRetrievalStrategyInterface;
+use Symfony\Component\Security\Authentication\AuthenticationTrustResolver;
+use Symfony\Component\Security\Role\RoleHierarchyInterface;
+use Symfony\Component\Security\Authorization\Voter\AuthenticatedVoter;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Domain/UserSecurityIdentity.php b/Acl/Domain/UserSecurityIdentity.php
new file mode 100644
index 0000000..ddc7566
--- /dev/null
+++ b/Acl/Domain/UserSecurityIdentity.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Domain;
+
+use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface;
+use Symfony\Component\Security\Authentication\Token\TokenInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Exception/AclAlreadyExistsException.php b/Acl/Exception/AclAlreadyExistsException.php
new file mode 100644
index 0000000..223b52c
--- /dev/null
+++ b/Acl/Exception/AclAlreadyExistsException.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Exception;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+class AclAlreadyExistsException extends Exception
+{
+} \ No newline at end of file
diff --git a/Acl/Exception/AclNotFoundException.php b/Acl/Exception/AclNotFoundException.php
new file mode 100644
index 0000000..140e739
--- /dev/null
+++ b/Acl/Exception/AclNotFoundException.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Exception;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+class AclNotFoundException extends Exception
+{
+} \ No newline at end of file
diff --git a/Acl/Exception/ConcurrentModificationException.php b/Acl/Exception/ConcurrentModificationException.php
new file mode 100644
index 0000000..fd65c2b
--- /dev/null
+++ b/Acl/Exception/ConcurrentModificationException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Exception;
+
+/**
+ * This exception is thrown whenever you change shared properties of more than
+ * one ACL of the same class type concurrently.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class ConcurrentModificationException extends Exception
+{
+} \ No newline at end of file
diff --git a/Acl/Exception/Exception.php b/Acl/Exception/Exception.php
new file mode 100644
index 0000000..0e0add3
--- /dev/null
+++ b/Acl/Exception/Exception.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Exception;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+class Exception extends \Exception
+{
+} \ No newline at end of file
diff --git a/Acl/Exception/InvalidDomainObjectException.php b/Acl/Exception/InvalidDomainObjectException.php
new file mode 100644
index 0000000..12f0b9a
--- /dev/null
+++ b/Acl/Exception/InvalidDomainObjectException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Exception;
+
+/**
+ * This exception is thrown when ObjectIdentity fails to construct an object
+ * identity from the passed domain object.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class InvalidDomainObjectException extends Exception
+{
+} \ No newline at end of file
diff --git a/Acl/Exception/NoAceFoundException.php b/Acl/Exception/NoAceFoundException.php
new file mode 100644
index 0000000..788be2a
--- /dev/null
+++ b/Acl/Exception/NoAceFoundException.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Exception;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+class NoAceFoundException extends Exception
+{
+} \ No newline at end of file
diff --git a/Acl/Exception/SidNotLoadedException.php b/Acl/Exception/SidNotLoadedException.php
new file mode 100644
index 0000000..c856dce
--- /dev/null
+++ b/Acl/Exception/SidNotLoadedException.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Exception;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+class SidNotLoadedException extends Exception
+{
+} \ No newline at end of file
diff --git a/Acl/Model/AclCacheInterface.php b/Acl/Model/AclCacheInterface.php
new file mode 100644
index 0000000..356006f
--- /dev/null
+++ b/Acl/Model/AclCacheInterface.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/AclInterface.php b/Acl/Model/AclInterface.php
new file mode 100644
index 0000000..d66e8da
--- /dev/null
+++ b/Acl/Model/AclInterface.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/AclProviderInterface.php b/Acl/Model/AclProviderInterface.php
new file mode 100644
index 0000000..238b687
--- /dev/null
+++ b/Acl/Model/AclProviderInterface.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/AuditLoggerInterface.php b/Acl/Model/AuditLoggerInterface.php
new file mode 100644
index 0000000..6540858
--- /dev/null
+++ b/Acl/Model/AuditLoggerInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/AuditableAclInterface.php b/Acl/Model/AuditableAclInterface.php
new file mode 100644
index 0000000..9c901d1
--- /dev/null
+++ b/Acl/Model/AuditableAclInterface.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/AuditableEntryInterface.php b/Acl/Model/AuditableEntryInterface.php
new file mode 100644
index 0000000..f829e88
--- /dev/null
+++ b/Acl/Model/AuditableEntryInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/DomainObjectInterface.php b/Acl/Model/DomainObjectInterface.php
new file mode 100644
index 0000000..2fa1aa6
--- /dev/null
+++ b/Acl/Model/DomainObjectInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+interface DomainObjectInterface
+{
+ /**
+ * Returns a unique identifier for this domain object.
+ *
+ * @return string
+ */
+ function getObjectIdentifier();
+} \ No newline at end of file
diff --git a/Acl/Model/EntryInterface.php b/Acl/Model/EntryInterface.php
new file mode 100644
index 0000000..476f18f
--- /dev/null
+++ b/Acl/Model/EntryInterface.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/FieldAwareEntryInterface.php b/Acl/Model/FieldAwareEntryInterface.php
new file mode 100644
index 0000000..545aa44
--- /dev/null
+++ b/Acl/Model/FieldAwareEntryInterface.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+interface FieldAwareEntryInterface
+{
+ function getField();
+} \ No newline at end of file
diff --git a/Acl/Model/MutableAclInterface.php b/Acl/Model/MutableAclInterface.php
new file mode 100644
index 0000000..305bb04
--- /dev/null
+++ b/Acl/Model/MutableAclInterface.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+use Doctrine\Common\NotifyPropertyChanged;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/MutableAclProviderInterface.php b/Acl/Model/MutableAclProviderInterface.php
new file mode 100644
index 0000000..3164af7
--- /dev/null
+++ b/Acl/Model/MutableAclProviderInterface.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/ObjectIdentityInterface.php b/Acl/Model/ObjectIdentityInterface.php
new file mode 100644
index 0000000..7f7dbc6
--- /dev/null
+++ b/Acl/Model/ObjectIdentityInterface.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/ObjectIdentityRetrievalStrategyInterface.php b/Acl/Model/ObjectIdentityRetrievalStrategyInterface.php
new file mode 100644
index 0000000..4709294
--- /dev/null
+++ b/Acl/Model/ObjectIdentityRetrievalStrategyInterface.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/**
+ * Retrieves the object identity for a given domain object
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+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/Acl/Model/PermissionGrantingStrategyInterface.php b/Acl/Model/PermissionGrantingStrategyInterface.php
new file mode 100644
index 0000000..5b7e03f
--- /dev/null
+++ b/Acl/Model/PermissionGrantingStrategyInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/SecurityIdentityInterface.php b/Acl/Model/SecurityIdentityInterface.php
new file mode 100644
index 0000000..251334d
--- /dev/null
+++ b/Acl/Model/SecurityIdentityInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Model/SecurityIdentityRetrievalStrategyInterface.php b/Acl/Model/SecurityIdentityRetrievalStrategyInterface.php
new file mode 100644
index 0000000..6a8bb4c
--- /dev/null
+++ b/Acl/Model/SecurityIdentityRetrievalStrategyInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Model;
+
+use Symfony\Component\Security\Authentication\Token\TokenInterface;
+
+/**
+ * Interface for retrieving security identities from tokens
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+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/Acl/Permission/BasicPermissionMap.php b/Acl/Permission/BasicPermissionMap.php
new file mode 100644
index 0000000..43a39d3
--- /dev/null
+++ b/Acl/Permission/BasicPermissionMap.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Permission;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Permission/MaskBuilder.php b/Acl/Permission/MaskBuilder.php
new file mode 100644
index 0000000..55aece4
--- /dev/null
+++ b/Acl/Permission/MaskBuilder.php
@@ -0,0 +1,202 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Permission;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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.
+ *
+ * <code>
+ * $builder = new MaskBuilder();
+ * $builder
+ * ->add('view')
+ * ->add('create')
+ * ->add('edit')
+ * ;
+ * var_dump($builder->get()); // int(7)
+ * var_dump($builder->getPattern()); // string(32) ".............................ECV"
+ * </code>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Permission/PermissionMapInterface.php b/Acl/Permission/PermissionMapInterface.php
new file mode 100644
index 0000000..27ee7f9
--- /dev/null
+++ b/Acl/Permission/PermissionMapInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Permission;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Voter/AclVoter.php b/Acl/Voter/AclVoter.php
new file mode 100644
index 0000000..954ad9b
--- /dev/null
+++ b/Acl/Voter/AclVoter.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Voter;
+
+use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
+use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
+use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
+use Symfony\Component\Security\Acl\Exception\NoAceFoundException;
+use Symfony\Component\Security\Acl\Exception\AclNotFoundException;
+use Symfony\Component\Security\Acl\Model\AclProviderInterface;
+use Symfony\Component\Security\Acl\Permission\PermissionMapInterface;
+use Symfony\Component\Security\Acl\Model\SecurityIdentityRetrievalStrategyInterface;
+use Symfony\Component\Security\Acl\Model\ObjectIdentityRetrievalStrategyInterface;
+use Symfony\Component\Security\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Authorization\Voter\VoterInterface;
+use Symfony\Component\Security\Role\RoleHierarchyInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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/Acl/Voter/FieldVote.php b/Acl/Voter/FieldVote.php
new file mode 100644
index 0000000..dbc4a61
--- /dev/null
+++ b/Acl/Voter/FieldVote.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Symfony\Component\Security\Acl\Voter;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * 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 <schmittjoh@gmail.com>
+ */
+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