diff options
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 |