From 12bcfbde5ea4a960f503b67aee8425e8ae3f907e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Thu, 12 Dec 2013 16:48:16 +0000 Subject: [PATCH] Added ACL related features --- Command/AbstractCommand.php | 24 +- Command/AclInitCommand.php | 124 ++++ Command/WrappedCommand.php | 23 - DependencyInjection/PropelExtension.php | 7 +- Model/Acl/AclClass.php | 43 ++ Model/Acl/AclClassQuery.php | 18 + Model/Acl/Entry.php | 80 +++ Model/Acl/EntryQuery.php | 70 +++ Model/Acl/ObjectIdentity.php | 141 +++++ Model/Acl/ObjectIdentityAncestor.php | 18 + Model/Acl/ObjectIdentityAncestorQuery.php | 18 + Model/Acl/ObjectIdentityQuery.php | 115 ++++ Model/Acl/SecurityIdentity.php | 87 +++ Model/Acl/SecurityIdentityQuery.php | 18 + Resources/acl_schema.xml | 104 ++++ Resources/config/security.xml | 21 + Security/Acl/AclProvider.php | 181 ++++++ Security/Acl/AuditableAclProvider.php | 39 ++ Security/Acl/Domain/Acl.php | 316 +++++++++++ Security/Acl/Domain/AuditableAcl.php | 103 ++++ Security/Acl/Domain/Entry.php | 192 +++++++ Security/Acl/Domain/FieldEntry.php | 101 ++++ Security/Acl/Domain/MutableAcl.php | 531 ++++++++++++++++++ Security/Acl/MutableAclProvider.php | 340 +++++++++++ Security/User/PropelUserProvider.php | 103 ++++ Tests/Fixtures/Model/User.php | 13 + .../Security/User/PropelUserProviderTest.php | 63 +++ 27 files changed, 2864 insertions(+), 29 deletions(-) create mode 100644 Command/AclInitCommand.php create mode 100644 Model/Acl/AclClass.php create mode 100644 Model/Acl/AclClassQuery.php create mode 100644 Model/Acl/Entry.php create mode 100644 Model/Acl/EntryQuery.php create mode 100644 Model/Acl/ObjectIdentity.php create mode 100644 Model/Acl/ObjectIdentityAncestor.php create mode 100644 Model/Acl/ObjectIdentityAncestorQuery.php create mode 100644 Model/Acl/ObjectIdentityQuery.php create mode 100644 Model/Acl/SecurityIdentity.php create mode 100644 Model/Acl/SecurityIdentityQuery.php create mode 100644 Resources/acl_schema.xml create mode 100644 Resources/config/security.xml create mode 100644 Security/Acl/AclProvider.php create mode 100644 Security/Acl/AuditableAclProvider.php create mode 100644 Security/Acl/Domain/Acl.php create mode 100644 Security/Acl/Domain/AuditableAcl.php create mode 100644 Security/Acl/Domain/Entry.php create mode 100644 Security/Acl/Domain/FieldEntry.php create mode 100644 Security/Acl/Domain/MutableAcl.php create mode 100644 Security/Acl/MutableAclProvider.php create mode 100644 Security/User/PropelUserProvider.php create mode 100644 Tests/Fixtures/Model/User.php create mode 100644 Tests/Security/User/PropelUserProviderTest.php diff --git a/Command/AbstractCommand.php b/Command/AbstractCommand.php index 4ba2bb3..d9462b0 100644 --- a/Command/AbstractCommand.php +++ b/Command/AbstractCommand.php @@ -12,6 +12,7 @@ namespace Propel\PropelBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Filesystem; @@ -125,7 +126,7 @@ abstract class AbstractCommand extends ContainerAwareCommand if ($this->input->hasOption('connection')) { $connections = $this->input->getOption('connection') ?: array($this->getContainer()->getParameter('propel.dbal.default_connection')); - if (!in_array($database['name'], $connections)) { + if (!in_array((string) $database['name'], $connections)) { // we skip this schema because the connection name doesn't // match the input values unset($this->tempSchemas[$tempSchema]); @@ -164,6 +165,27 @@ abstract class AbstractCommand extends ContainerAwareCommand return $this->getSchemaLocator()->locateFromBundles($kernel->getBundles()); } + protected function runCommand(Command $command, array $parameters, InputInterface $input, OutputInterface $output) + { + // add the command's name to the parameters + array_unshift($parameters, $this->getName()); + + // merge the default parameters + $parameters = array_merge(array( + '--input-dir' => $this->cacheDir, + '--verbose' => $input->getOption('verbose'), + ), $parameters); + + if ($input->hasOption('platform')) { + $parameters['--platform'] = $input->getOption('platform'); + } + + $command->setApplication($this->getApplication()); + + // and run the sub-command + return $command->run(new ArrayInput($parameters), $output); + } + /* * Create an XML file which represents propel.configuration * diff --git a/Command/AclInitCommand.php b/Command/AclInitCommand.php new file mode 100644 index 0000000..e535ba3 --- /dev/null +++ b/Command/AclInitCommand.php @@ -0,0 +1,124 @@ + + */ +class AclInitCommand extends AbstractCommand +{ + protected function configure() + { + $this + ->setDescription('Initialize "Access Control Lists" model and SQL') + ->addOption('force', null, InputOption::VALUE_NONE, 'Set this parameter to execute this action.') + ->addOption('connection', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Connection to use. Example: default, bookstore') + ->setHelp(<<%command.name% command connects to the database and executes all SQL statements required to setup the ACL database, it also generates the ACL model. + + php %command.full_name% + +The --force parameter has to be used to actually insert SQL. +The --connection parameter allows you to change the connection to use. +The default connection is the active connection (propel.dbal.default_connection). +EOT + ) + ->setName('propel:acl:init') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $outputDir = realpath($this->getApplication()->getKernel()->getRootDir().'/../'); + + // Generate ACL model + $modelBuildCmd = new \Propel\Generator\Command\ModelBuildCommand(); + $modelBuildArgs = array( + '--output-dir' => $outputDir, + ); + + if ($this->runCommand($modelBuildCmd, $modelBuildArgs, $input, $output) === 0) { + $output->writeln(sprintf( + '>> %20s Generated model classes from %s', + $this->getApplication()->getKernel()->getBundle('PropelBundle')->getName(), + 'acl_schema.xml' + )); + } else { + $this->writeTaskError($output, 'model:build'); + + return 1; + } + + // Prepare SQL + $sqlBuildCmd = new \Propel\Generator\Command\SqlBuildCommand(); + $sqlBuildArgs = array( + '--connection' => $this->getConnections($input->getOption('connection')), + '--output-dir' => $this->getCacheDir(), + ); + + if ($this->runCommand($sqlBuildCmd, $sqlBuildArgs, $input, $output) === 0) { + $this->writeSection( + $output, + '1 SQL file has been generated.' + ); + } else { + $this->writeTaskError($output, 'sql:build'); + + return 2; + } + + // insert sql + $sqlInsertCmd = new \Propel\Generator\Command\SqlInsertCommand(); + $sqlInsertArgs = array( + '--connection' => $this->getConnections($input->getOption('connection')), + ); + + if ($this->runCommand($sqlBuildCmd, $sqlBuildArgs, $input, $output) === 0) { + $this->writeSection( + $output, + '1 SQL file has been inserted.' + ); + } else { + $this->writeTaskError($output, 'sql:insert'); + + return 3; + } + } + + protected function getFinalSchemas(KernelInterface $kernel, BundleInterface $bundle = null) + { + $aclSchema = new \SplFileInfo($kernel->locateResource('@PropelBundle/Resources/acl_schema.xml')); + + return array( + array($kernel->getBundle('PropelBundle'), $aclSchema) + ); + } + + /** + * {@inheritdoc} + */ + protected function initialize(InputInterface $input, OutputInterface $output) + { + parent::initialize($input, $output); + + $this->cacheDir = $this->cacheDir . '/acl'; + + $this->setupBuildTimeFiles(); + } +} diff --git a/Command/WrappedCommand.php b/Command/WrappedCommand.php index 6d2fbc4..06ba36a 100644 --- a/Command/WrappedCommand.php +++ b/Command/WrappedCommand.php @@ -12,8 +12,6 @@ namespace Propel\PropelBundle\Command; use Propel\Generator\Command\AbstractCommand as BaseCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -60,25 +58,4 @@ abstract class WrappedCommand extends AbstractCommand return $this->runCommand($command, $params, $input, $output); } - - protected function runCommand(Command $command, array $parameters, InputInterface $input, OutputInterface $output) - { - // add the command's name to the parameters - array_unshift($parameters, $this->getName()); - - // merge the default parameters - $parameters = array_merge(array( - '--input-dir' => $this->cacheDir, - '--verbose' => $input->getOption('verbose'), - ), $parameters); - - if ($input->hasOption('platform')) { - $parameters['--platform'] = $input->getOption('platform'); - } - - $command->setApplication($this->getApplication()); - - // and run the sub-command - return $command->run(new ArrayInput($parameters), $output); - } } diff --git a/DependencyInjection/PropelExtension.php b/DependencyInjection/PropelExtension.php index 3888bb9..8b93c9b 100644 --- a/DependencyInjection/PropelExtension.php +++ b/DependencyInjection/PropelExtension.php @@ -36,11 +36,7 @@ class PropelExtension extends Extension $configuration = $this->getConfiguration($configs, $container); $config = $processor->processConfiguration($configuration, $configs); - if (isset($config['logging']) && $config['logging']) { - $logging = $config['logging']; - } else { - $logging = false; - } + $logging = isset($config['logging']) && $config['logging']; $container->setParameter('propel.logging', $logging); @@ -49,6 +45,7 @@ class PropelExtension extends Extension $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('propel.xml'); $loader->load('converters.xml'); + $loader->load('security.xml'); } // build properties diff --git a/Model/Acl/AclClass.php b/Model/Acl/AclClass.php new file mode 100644 index 0000000..85b184f --- /dev/null +++ b/Model/Acl/AclClass.php @@ -0,0 +1,43 @@ +filterByType($objectIdentity->getType()) + ->findOneOrCreate($con) + ; + + if ($obj->isNew()) { + $obj->save($con); + } + + return $obj; + } +} diff --git a/Model/Acl/AclClassQuery.php b/Model/Acl/AclClassQuery.php new file mode 100644 index 0000000..741bdc4 --- /dev/null +++ b/Model/Acl/AclClassQuery.php @@ -0,0 +1,18 @@ +getId()) { + $entry->setId($aclEntry->getId()); + } + + $entry + ->setMask($aclEntry->getMask()) + ->setGranting($aclEntry->isGranting()) + ->setGrantingStrategy($aclEntry->getStrategy()) + ->setSecurityIdentity(SecurityIdentity::fromAclIdentity($aclEntry->getSecurityIdentity())) + ; + + if ($aclEntry instanceof FieldEntryInterface) { + $entry->setFieldName($aclEntry->getField()); + } + + if ($aclEntry instanceof AuditableEntryInterface) { + $entry + ->setAuditFailure($aclEntry->isAuditFailure()) + ->setAuditSuccess($aclEntry->isAuditSuccess()) + ; + } + + return $entry; + } + + /** + * Transform a given model entry into an ACL related Entry (ACE). + * + * @param \Propel\PropelBundle\Model\Acl\Entry $modelEntry + * @param \Symfony\Component\Security\Acl\Model\AclInterface $acl + * + * @return \Symfony\Component\Security\Acl\Model\EntryInterface + */ + public static function toAclEntry(Entry $modelEntry, AclInterface $acl) + { + if (null === $modelEntry->getFieldName()) { + return new AclEntry($modelEntry, $acl); + } + + return new AclFieldEntry($modelEntry, $acl); + } +} diff --git a/Model/Acl/EntryQuery.php b/Model/Acl/EntryQuery.php new file mode 100644 index 0000000..0f6b32f --- /dev/null +++ b/Model/Acl/EntryQuery.php @@ -0,0 +1,70 @@ +getId()] = $securityIdentity->getId(); + } + } + + $this + ->useAclClassQuery(null, Criteria::INNER_JOIN) + ->filterByType((string) $objectIdentity->getType()) + ->endUse() + ->leftJoinObjectIdentity() + ->add(ObjectIdentityTableMap::OBJECT_IDENTIFIER, (string) $objectIdentity->getIdentifier(), Criteria::EQUAL) + ->addOr(EntryTableMap::OBJECT_IDENTITY_ID, null, Criteria::ISNULL) + ; + + if (!empty($securityIdentities)) { + $this->filterBySecurityIdentityId($securityIds); + } + + return $this->find($con); + } +} diff --git a/Model/Acl/ObjectIdentity.php b/Model/Acl/ObjectIdentity.php new file mode 100644 index 0000000..c7bc32a --- /dev/null +++ b/Model/Acl/ObjectIdentity.php @@ -0,0 +1,141 @@ +setObjectIdentityRelatedByObjectIdentityId($this); + $ancestor->setObjectIdentityRelatedByAncestorId($this); + + $this->addObjectIdentityAncestorRelatedByAncestorId($ancestor); + + if ($this->getParentObjectIdentityId()) { + $this->updateAncestorsTree($con); + } + + return true; + } + + public function preUpdate(ConnectionInterface $con = null) + { + if ($this->isColumnModified(ObjectIdentityTableMap::PARENT_OBJECT_IDENTITY_ID)) { + $this->updateAncestorsTree($con); + } + + return true; + } + + public function preDelete(ConnectionInterface $con = null) + { + // Only retrieve direct children, it's faster and grand children will be retrieved recursively. + $children = ObjectIdentityQuery::create()->findChildren($this, $con); + + $objIds = $children->getPrimaryKeys(false); + $objIds[] = $this->getId(); + + $children->delete($con); + + // Manually delete those for DBAdapter not capable of cascading the DELETE. + ObjectIdentityAncestorQuery::create() + ->filterByObjectIdentityId($objIds, Criteria::IN) + ->delete($con) + ; + + return true; + } + + /** + * Update all ancestor entries to reflect changes on this instance. + * + * @param ConnectionInterface $con + * + * @return \Propel\PropelBundle\Model\Acl\ObjectIdentity $this + */ + protected function updateAncestorsTree(ConnectionInterface $con = null) + { + $con->beginTransaction(); + + $oldAncestors = ObjectIdentityQuery::create()->findAncestors($this, $con); + + $children = ObjectIdentityQuery::create()->findGrandChildren($this, $con); + $children->append($this); + + if (count($oldAncestors)) { + foreach ($children as $eachChild) { + /* + * Delete only those entries, that are ancestors based on the parent relation. + * Ancestors of grand children up to the current node will be kept. + */ + $query = ObjectIdentityAncestorQuery::create() + ->filterByObjectIdentityId($eachChild->getId()) + ->filterByObjectIdentityRelatedByAncestorId($oldAncestors, Criteria::IN) + ; + + if ($eachChild->getId() !== $this->getId()) { + $query->filterByAncestorId(array($eachChild->getId(), $this->getId()), Criteria::NOT_IN); + } else { + $query->filterByAncestorId($this->getId(), Criteria::NOT_EQUAL); + } + + $query->delete($con); + } + } + + // This is the new parent object identity! + $parent = $this->getObjectIdentityRelatedByParentObjectIdentityId($con); + if (null !== $parent) { + $newAncestors = ObjectIdentityQuery::create()->findAncestors($parent, $con); + $newAncestors->append($parent); + foreach ($newAncestors as $eachAncestor) { + // This collection contains the current object identity! + foreach ($children as $eachChild) { + $ancestor = ObjectIdentityAncestorQuery::create() + ->filterByObjectIdentityId($eachChild->getId()) + ->filterByAncestorId($eachAncestor->getId()) + ->findOneOrCreate($con) + ; + + // If the entry already exists, next please. + if (!$ancestor->isNew()) { + continue; + } + + if ($eachChild->getId() === $this->getId()) { + // Do not save() here, as it would result in an infinite recursion loop! + $this->addObjectIdentityAncestorRelatedByObjectIdentityId($ancestor); + } else { + // Save the new ancestor to avoid integrity constraint violation. + $ancestor->save($con); + + $eachChild + ->addObjectIdentityAncestorRelatedByObjectIdentityId($ancestor) + ->save($con) + ; + } + } + } + } + + $con->commit(); + + return $this; + } +} diff --git a/Model/Acl/ObjectIdentityAncestor.php b/Model/Acl/ObjectIdentityAncestor.php new file mode 100644 index 0000000..5dff389 --- /dev/null +++ b/Model/Acl/ObjectIdentityAncestor.php @@ -0,0 +1,18 @@ +filterByClassId($aclClass->getId()) + ->filterByIdentifier($objectIdentity->getIdentifier()) + ; + + return $this; + } + + /** + * Return an ObjectIdentity object belonging to the given ACL related ObjectIdentity. + * + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * @param ConnectionInterface $con + * + * @return \Propel\PropelBundle\Model\Acl\ObjectIdentity + */ + public function findOneByAclObjectIdentity(ObjectIdentityInterface $objectIdentity, ConnectionInterface $con = null) + { + return $this + ->filterByAclObjectIdentity($objectIdentity, $con) + ->findOne($con) + ; + } + + /** + * Return all children of the given object identity. + * + * @param \Propel\PropelBundle\Model\Acl\ObjectIdentity $objectIdentity + * @param ConnectionInterface $con + * + * @return \PropelObjectCollection + */ + public function findChildren(ObjectIdentity $objectIdentity, ConnectionInterface $con = null) + { + return $this + ->filterByObjectIdentityRelatedByParentObjectIdentityId($objectIdentity) + ->find($con) + ; + } + + /** + * Return all children and grand-children of the given object identity. + * + * @param \Propel\PropelBundle\Model\Acl\ObjectIdentity $objectIdentity + * @param ConnectionInterface $con + * + * @return \PropelObjectCollection + */ + public function findGrandChildren(ObjectIdentity $objectIdentity, ConnectionInterface $con = null) + { + return $this + ->useObjectIdentityAncestorRelatedByObjectIdentityIdQuery() + ->filterByObjectIdentityRelatedByAncestorId($objectIdentity) + ->filterByObjectIdentityRelatedByObjectIdentityId($objectIdentity, Criteria::NOT_EQUAL) + ->endUse() + ->find($con) + ; + } + + /** + * Return all ancestors of the given object identity. + * + * @param ObjectIdentity $objectIdentity + * @param ConnectionInterface $con + * + * @return \PropelObjectCollection + */ + public function findAncestors(ObjectIdentity $objectIdentity, ConnectionInterface $con = null) + { + return $this + ->useObjectIdentityAncestorRelatedByAncestorIdQuery() + ->filterByObjectIdentityRelatedByObjectIdentityId($objectIdentity) + ->filterByObjectIdentityRelatedByAncestorId($objectIdentity, Criteria::NOT_EQUAL) + ->endUse() + ->find($con) + ; + } +} diff --git a/Model/Acl/SecurityIdentity.php b/Model/Acl/SecurityIdentity.php new file mode 100644 index 0000000..7ceae90 --- /dev/null +++ b/Model/Acl/SecurityIdentity.php @@ -0,0 +1,87 @@ +getIdentifier(); + + if ($securityIdentity->getUsername()) { + if (false === strpos($identifier, '-')) { + throw new \InvalidArgumentException('The given identifier does not resolve to a UserSecurityIdentity.'); + } + + list($class, $username) = explode('-', $identifier, 2); + + return new UserSecurityIdentity($username, $class); + } + + if (0 === strpos($identifier, 'ROLE_') or 0 === strpos($identifier, 'IS_AUTHENTICATED_')) { + return new RoleSecurityIdentity($identifier); + } + + throw new \InvalidArgumentException('The security identity does not resolve to either UserSecurityIdentity or RoleSecurityIdentity.'); + } + + /** + * Transform a given ACL security identity into a SecurityIdentity model. + * + * If there is no model entry given, a new one will be created and saved to the database. + * + * @throws \InvalidArgumentException + * + * @param \Symfony\Component\Security\Acl\Model\SecurityIdentityInterface $aclIdentity + * @param ConnectionInterface $con + * + * @return \Propel\PropelBundle\Model\Acl\SecurityIdentity + */ + public static function fromAclIdentity(SecurityIdentityInterface $aclIdentity, ConnectionInterface $con = null) + { + if ($aclIdentity instanceof UserSecurityIdentity) { + $identifier = $aclIdentity->getClass().'-'.$aclIdentity->getUsername(); + $username = true; + } elseif ($aclIdentity instanceof RoleSecurityIdentity) { + $identifier = $aclIdentity->getRole(); + $username = false; + } else { + throw new \InvalidArgumentException('The ACL identity must either be an instance of UserSecurityIdentity or RoleSecurityIdentity.'); + } + + $obj = SecurityIdentityQuery::create() + ->filterByIdentifier($identifier) + ->filterByUsername($username) + ->findOneOrCreate($con) + ; + + if ($obj->isNew()) { + $obj->save($con); + } + + return $obj; + } +} diff --git a/Model/Acl/SecurityIdentityQuery.php b/Model/Acl/SecurityIdentityQuery.php new file mode 100644 index 0000000..63044ba --- /dev/null +++ b/Model/Acl/SecurityIdentityQuery.php @@ -0,0 +1,18 @@ + + + + + + + + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/Resources/config/security.xml b/Resources/config/security.xml new file mode 100644 index 0000000..6ef4b98 --- /dev/null +++ b/Resources/config/security.xml @@ -0,0 +1,21 @@ + + + + + + Propel\PropelBundle\Security\Acl\AuditableAclProvider + Propel\PropelBundle\Security\User\PropelUserProvider + + + + + + + + + + + + diff --git a/Security/Acl/AclProvider.php b/Security/Acl/AclProvider.php new file mode 100644 index 0000000..6ac4510 --- /dev/null +++ b/Security/Acl/AclProvider.php @@ -0,0 +1,181 @@ + + */ +class AclProvider implements AclProviderInterface +{ + protected $permissionGrantingStrategy; + protected $connection; + protected $cache; + + /** + * Constructor. + * + * @param \Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface $permissionGrantingStrategy + * @param ConnectionInterface $con + * @param \Symfony\Component\Security\Acl\Model\AclCacheInterface $cache + */ + public function __construct(PermissionGrantingStrategyInterface $permissionGrantingStrategy, ConnectionInterface $connection = null, AclCacheInterface $cache = null) + { + $this->permissionGrantingStrategy = $permissionGrantingStrategy; + $this->connection = $connection; + $this->cache = $cache; + } + + /** + * Retrieves all child object identities from the database. + * + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $parentObjectIdentity + * @param bool $directChildrenOnly + * + * @return array + */ + public function findChildren(ObjectIdentityInterface $parentObjectIdentity, $directChildrenOnly = false) + { + $modelIdentity = ObjectIdentityQuery::create()->findOneByAclObjectIdentity($parentObjectIdentity, $this->connection); + if (empty($modelIdentity)) { + return array(); + } + + if ($directChildrenOnly) { + $collection = ObjectIdentityQuery::create()->findChildren($modelIdentity, $this->connection); + } else { + $collection = ObjectIdentityQuery::create()->findGrandChildren($modelIdentity, $this->connection); + } + + $children = array(); + foreach ($collection as $eachChild) { + $children[] = new ObjectIdentity($eachChild->getIdentifier(), $eachChild->getAclClass($this->connection)->getType()); + } + + return $children; + } + + /** + * Returns the ACL that belongs to the given object identity + * + * @throws \Symfony\Component\Security\Acl\Exception\AclNotFoundException + * + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * @param array $securityIdentities + * + * @return \Symfony\Component\Security\Acl\Model\AclInterface + */ + public function findAcl(ObjectIdentityInterface $objectIdentity, array $securityIdentities = array()) + { + $modelObj = ObjectIdentityQuery::create()->findOneByAclObjectIdentity($objectIdentity, $this->connection); + if (null !== $this->cache and null !== $modelObj) { + $cachedAcl = $this->cache->getFromCacheById($modelObj->getId()); + if ($cachedAcl instanceof AclInterface) { + return $cachedAcl; + } + } + + $collection = EntryQuery::create()->findByAclIdentity($objectIdentity, $securityIdentities, $this->connection); + + if (0 === count($collection)) { + if (empty($securityIdentities)) { + $errorMessage = 'There is no ACL available for this object identity. Please create one using the MutableAclProvider.'; + } else { + $errorMessage = 'There is at least no ACL for this object identity and the given security identities. Try retrieving the ACL without security identity filter and add ACEs for the security identities.'; + } + + throw new AclNotFoundException($errorMessage); + } + + $loadedSecurityIdentities = array(); + foreach ($collection as $eachEntry) { + if (!isset($loadedSecurityIdentities[$eachEntry->getSecurityIdentity()->getId()])) { + $loadedSecurityIdentities[$eachEntry->getSecurityIdentity()->getId()] = SecurityIdentity::toAclIdentity($eachEntry->getSecurityIdentity()); + } + } + + $parentAcl = null; + $entriesInherited = true; + + if (null !== $modelObj) { + $entriesInherited = $modelObj->getEntriesInheriting(); + if (null !== $modelObj->getParentObjectIdentityId()) { + $parentObj = $modelObj->getObjectIdentityRelatedByParentObjectIdentityId($this->connection); + try { + $parentAcl = $this->findAcl(new ObjectIdentity($parentObj->getIdentifier(), $parentObj->getAclClass($this->connection)->getType())); + } catch (AclNotFoundException $e) { + /* + * This happens e.g. if the parent ACL is created, but does not contain any ACE by now. + * The ACEs may be applied later on. + */ + } + } + } + + return $this->getAcl($collection, $objectIdentity, $loadedSecurityIdentities, $parentAcl, $entriesInherited); + } + + /** + * Returns the ACLs that belong to the given object identities + * + * @throws \Symfony\Component\Security\Acl\Exception\AclNotFoundException When at least one object identity is missing its ACL. + * + * @param array $objectIdentities an array of ObjectIdentityInterface implementations + * @param array $securityIdentities an array of SecurityIdentityInterface implementations + * + * @return \SplObjectStorage mapping the passed object identities to ACLs + */ + public function findAcls(array $objectIdentities, array $securityIdentities = array()) + { + $result = new \SplObjectStorage(); + foreach ($objectIdentities as $eachIdentity) { + $result[$eachIdentity] = $this->findAcl($eachIdentity, $securityIdentities); + } + + return $result; + } + + /** + * Create an ACL. + * + * @param ObjectCollection $collection + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * @param array $loadedSecurityIdentities + * @param \Symfony\Component\Security\Acl\Model\AclInterface $parentAcl + * @param bool $inherited + * + * @return \Propel\PropelBundle\Security\Acl\Domain\Acl + */ + protected function getAcl(ObjectCollection $collection, ObjectIdentityInterface $objectIdentity, array $loadedSecurityIdentities = array(), AclInterface $parentAcl = null, $inherited = true) + { + return new Acl($collection, $objectIdentity, $this->permissionGrantingStrategy, $loadedSecurityIdentities, $parentAcl, $inherited); + } +} diff --git a/Security/Acl/AuditableAclProvider.php b/Security/Acl/AuditableAclProvider.php new file mode 100644 index 0000000..c8531e8 --- /dev/null +++ b/Security/Acl/AuditableAclProvider.php @@ -0,0 +1,39 @@ + + */ +class AuditableAclProvider extends MutableAclProvider +{ + /** + * Get an ACL for this provider. + * + * @param Propel\Runtime\Collection\ObjectCollection $collection + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * @param array $loadedSecurityIdentities + * @param \Symfony\Component\Security\Acl\Model\AclInterface $parentAcl + * @param bool $inherited + * + * @return \Propel\PropelBundle\Security\Acl\Domain\AuditableAcl + */ + protected function getAcl(ObjectCollection $collection, ObjectIdentityInterface $objectIdentity, array $loadedSecurityIdentities = array(), AclInterface $parentAcl = null, $inherited = true) + { + return new AuditableAcl($collection, $objectIdentity, $this->permissionGrantingStrategy, $loadedSecurityIdentities, $parentAcl, $inherited, $this->connection); + } +} diff --git a/Security/Acl/Domain/Acl.php b/Security/Acl/Domain/Acl.php new file mode 100644 index 0000000..01cf057 --- /dev/null +++ b/Security/Acl/Domain/Acl.php @@ -0,0 +1,316 @@ + + */ +class Acl implements AclInterface +{ + protected $model = 'Propel\PropelBundle\Model\Acl\Entry'; + + protected $classAces = array(); + protected $classFieldAces = array(); + protected $objectAces = array(); + protected $objectFieldAces = array(); + + protected $objectIdentity; + protected $parentAcl; + protected $permissionGrantingStrategy; + protected $inherited; + + protected $loadedSecurityIdentities = array(); + + /** + * A list of known associated fields on this ACL. + * + * @var array + */ + protected $fields = array(); + + /** + * Constructor. + * + * @param ObjectCollection $entries + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * @param \Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface $permissionGrantingStrategy + * @param array $loadedSecurityIdentities + * @param \Symfony\Component\Security\Acl\Model\AclInterface $parentAcl + * @param bool $inherited + */ + public function __construct(ObjectCollection $entries, ObjectIdentityInterface $objectIdentity, PermissionGrantingStrategyInterface $permissionGrantingStrategy, array $loadedSecurityIdentities = array(), AclInterface $parentAcl = null, $inherited = true) + { + if ($entries->getModel() !== $this->model) { + throw new AclException(sprintf('The given collection does not contain models of class "%s" but of class "%s".', $this->model, $entries->getModel())); + } + + foreach ($entries as $eachEntry) { + if (null === $eachEntry->getFieldName() and null === $eachEntry->getObjectIdentityId()) { + $this->classAces[] = new Entry($eachEntry, $this); + } + + if (null !== $eachEntry->getFieldName() and null === $eachEntry->getObjectIdentityId()) { + if (empty($this->classFieldAces[$eachEntry->getFieldName()])) { + $this->classFieldAces[$eachEntry->getFieldName()] = array(); + $this->updateFields($eachEntry->getFieldName()); + } + + $this->classFieldAces[$eachEntry->getFieldName()][] = new FieldEntry($eachEntry, $this); + } + + if (null === $eachEntry->getFieldName() and null !== $eachEntry->getObjectIdentityId()) { + $this->objectAces[] = new Entry($eachEntry, $this); + } + + if (null !== $eachEntry->getFieldName() and null !== $eachEntry->getObjectIdentityId()) { + if (empty($this->objectFieldAces[$eachEntry->getFieldName()])) { + $this->objectFieldAces[$eachEntry->getFieldName()] = array(); + $this->updateFields($eachEntry->getFieldName()); + } + + $this->objectFieldAces[$eachEntry->getFieldName()][] = new FieldEntry($eachEntry, $this); + } + } + + $this->objectIdentity = $objectIdentity; + $this->permissionGrantingStrategy = $permissionGrantingStrategy; + $this->parentAcl = $parentAcl; + $this->inherited = $inherited; + $this->loadedSecurityIdentities = $loadedSecurityIdentities; + + $this->fields = array_unique($this->fields); + } + + /** + * Returns all class-based ACEs associated with this ACL + * + * @return array + */ + public function getClassAces() + { + return $this->classAces; + } + + /** + * Returns all class-field-based ACEs associated with this ACL + * + * @param string $field + * + * @return array + */ + public function getClassFieldAces($field) + { + return isset($this->classFieldAces[$field]) ? $this->classFieldAces[$field] : array(); + } + + /** + * Returns all object-based ACEs associated with this ACL + * + * @return array + */ + public function getObjectAces() + { + return $this->objectAces; + } + + /** + * Returns all object-field-based ACEs associated with this ACL + * + * @param string $field + * + * @return array + */ + public function getObjectFieldAces($field) + { + return isset($this->objectFieldAces[$field]) ? $this->objectFieldAces[$field] : array(); + } + + /** + * Returns the object identity associated with this ACL + * + * @return \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface + */ + public function getObjectIdentity() + { + return $this->objectIdentity; + } + + /** + * Returns the parent ACL, or null if there is none. + * + * @return \Symfony\Component\Security\Acl\Model\AclInterface|null + */ + public function getParentAcl() + { + return $this->parentAcl; + } + + /** + * Whether this ACL is inheriting ACEs from a parent ACL. + * + * @return bool + */ + public function isEntriesInheriting() + { + return $this->inherited; + } + + /** + * Determines whether field access is granted + * + * @param string $field + * @param array $masks + * @param array $securityIdentities + * @param bool $administrativeMode + * + * @return bool + */ + public function isFieldGranted($field, array $masks, array $securityIdentities, $administrativeMode = false) + { + return $this->permissionGrantingStrategy->isFieldGranted($this, $field, $masks, $securityIdentities, $administrativeMode); + } + + /** + * Determines whether access is granted + * + * @throws \Symfony\Component\Security\Acl\Exception\NoAceFoundException when no ACE was applicable for this request + * + * @param array $masks + * @param array $securityIdentities + * @param bool $administrativeMode + * + * @return bool + */ + public function isGranted(array $masks, array $securityIdentities, $administrativeMode = false) + { + return $this->permissionGrantingStrategy->isGranted($this, $masks, $securityIdentities, $administrativeMode); + } + + /** + * Whether the ACL has loaded ACEs for all of the passed security identities + * + * @throws \InvalidArgumentException + * + * @param mixed $securityIdentities an implementation of SecurityIdentityInterface, or an array thereof + * + * @return bool + */ + public function isSidLoaded($securityIdentities) + { + if (!is_array($securityIdentities)) { + $securityIdentities = array($securityIdentities); + } + + $found = 0; + foreach ($securityIdentities as $eachSecurityIdentity) { + if (!$eachSecurityIdentity instanceof SecurityIdentityInterface) { + throw new \InvalidArgumentException('At least one entry of the given list is not implementing the "SecurityIdentityInterface".'); + } + + foreach ($this->loadedSecurityIdentities as $eachLoadedIdentity) { + if ($eachSecurityIdentity->equals($eachLoadedIdentity)) { + $found++; + + break; + } + } + } + + return ($found === count($securityIdentities)); + } + + /** + * String representation of object + * + * @link http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or &null; + */ + public function serialize() + { + return serialize(array( + $this->model, + $this->classAces, + $this->classFieldAces, + $this->objectAces, + $this->objectFieldAces, + $this->objectIdentity, + $this->parentAcl, + $this->permissionGrantingStrategy, + $this->inherited, + $this->loadedSecurityIdentities, + )); + } + + /** + * Constructs the object + * + * @link http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized + * + * @return mixed the original value unserialized. + */ + public function unserialize($serialized) + { + list( + $this->model, + $this->classAces, + $this->classFieldAces, + $this->objectAces, + $this->objectFieldAces, + $this->objectIdentity, + $this->parentAcl, + $this->permissionGrantingStrategy, + $this->inherited, + $this->loadedSecurityIdentities, + ) = unserialize($serialized); + + return $this; + } + + /** + * Returns a list of associated fields on this ACL. + * + * @return array + */ + public function getFields() + { + return $this->fields; + } + + /** + * Update the internal list of associated fields on this ACL. + * + * @param string $field + * + * @return \Propel\PropelBundle\Security\Acl\Domain\Acl $this + */ + protected function updateFields($field) + { + if (!in_array($field, $this->fields)) { + $this->fields[] = $field; + } + + return $this; + } +} diff --git a/Security/Acl/Domain/AuditableAcl.php b/Security/Acl/Domain/AuditableAcl.php new file mode 100644 index 0000000..1eef18e --- /dev/null +++ b/Security/Acl/Domain/AuditableAcl.php @@ -0,0 +1,103 @@ + + */ +class AuditableAcl extends MutableAcl implements AuditableAclInterface +{ + /** + * Updates auditing for class-based ACE + * + * @param integer $index + * @param bool $auditSuccess + * @param bool $auditFailure + */ + public function updateClassAuditing($index, $auditSuccess, $auditFailure) + { + $this->updateAuditing($this->classAces, $index, $auditSuccess, $auditFailure); + } + + /** + * Updates auditing for class-field-based ACE + * + * @param integer $index + * @param string $field + * @param bool $auditSuccess + * @param bool $auditFailure + */ + public function updateClassFieldAuditing($index, $field, $auditSuccess, $auditFailure) + { + $this->validateField($this->classFieldAces, $field); + $this->updateAuditing($this->classFieldAces[$field], $index, $auditSuccess, $auditFailure); + } + + /** + * Updates auditing for object-based ACE + * + * @param integer $index + * @param bool $auditSuccess + * @param bool $auditFailure + */ + public function updateObjectAuditing($index, $auditSuccess, $auditFailure) + { + $this->updateAuditing($this->objectAces, $index, $auditSuccess, $auditFailure); + } + + /** + * Updates auditing for object-field-based ACE + * + * @param integer $index + * @param string $field + * @param bool $auditSuccess + * @param bool $auditFailure + */ + public function updateObjectFieldAuditing($index, $field, $auditSuccess, $auditFailure) + { + $this->validateField($this->objectFieldAces, $field); + $this->updateAuditing($this->objectFieldAces[$field], $index, $auditSuccess, $auditFailure); + } + + /** + * Update auditing on a single ACE. + * + * @throws \InvalidArgumentException + * + * @param array $list + * @param int $index + * @param bool $auditSuccess + * @param bool $auditFailure + * + * @return \Propel\PropelBundle\Security\Acl\Domain\AuditableAcl $this + */ + protected function updateAuditing(array &$list, $index, $auditSuccess, $auditFailure) + { + if (!is_bool($auditSuccess) or !is_bool($auditFailure)) { + throw new \InvalidArgumentException('The given auditing flags are invalid. Please provide boolean only.'); + } + + $this->validateIndex($list, $index); + + $entry = ModelEntry::fromAclEntry($list[$index]) + ->setAuditSuccess($auditSuccess) + ->setAuditFailure($auditFailure) + ; + + $list[$index] = ModelEntry::toAclEntry($entry, $this); + + return $this; + } +} diff --git a/Security/Acl/Domain/Entry.php b/Security/Acl/Domain/Entry.php new file mode 100644 index 0000000..b9a641d --- /dev/null +++ b/Security/Acl/Domain/Entry.php @@ -0,0 +1,192 @@ + + */ +class Entry implements AuditableEntryInterface +{ + protected $acl; + + protected $id; + protected $securityIdentity; + protected $mask; + protected $isGranting; + protected $strategy; + protected $auditSuccess; + protected $auditFailure; + + /** + * Constructor. + * + * @param \Propel\PropelBundle\Model\Acl\Entry $entry + * @param \Symfony\Component\Security\Acl\Model\AclInterface $acl + */ + public function __construct(ModelEntry $entry, AclInterface $acl) + { + $this->acl = $acl; + $this->securityIdentity = SecurityIdentity::toAclIdentity($entry->getSecurityIdentity()); + + /* + * A new ACE (from a MutableAcl) does not have an ID, + * but will be persisted by the MutableAclProvider afterwards, if issued. + */ + if ($entry->getId()) { + $this->id = $entry->getId(); + } + + $this->mask = $entry->getMask(); + $this->isGranting = $entry->getGranting(); + $this->strategy = $entry->getGrantingStrategy(); + $this->auditFailure = $entry->getAuditFailure(); + $this->auditSuccess = $entry->getAuditSuccess(); + } + + /** + * String representation of object + * + * @link http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or &null; + */ + public function serialize() + { + return serialize(array( + $this->acl, + $this->securityIdentity, + $this->id, + $this->mask, + $this->isGranting, + $this->strategy, + $this->auditFailure, + $this->auditSuccess, + )); + } + + /** + * Constructs the object + * + * @link http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized + * + * @return mixed the original value unserialized. + */ + public function unserialize($serialized) + { + list( + $this->acl, + $this->securityIdentity, + $this->id, + $this->mask, + $this->isGranting, + $this->strategy, + $this->auditFailure, + $this->auditSuccess, + ) = unserialize($serialized); + + return $this; + } + + /** + * The ACL this ACE is associated with. + * + * @return \Symfony\Component\Security\Acl\Model\AclInterface + */ + public function getAcl() + { + return $this->acl; + } + + /** + * The security identity associated with this ACE + * + * @return \Symfony\Component\Security\Acl\Model\SecurityIdentityInterface + */ + public function getSecurityIdentity() + { + return $this->securityIdentity; + } + + /** + * The primary key of this ACE + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * The permission mask of this ACE + * + * @return integer + */ + public function getMask() + { + return $this->mask; + } + + /** + * The strategy for comparing masks + * + * @return string + */ + public function getStrategy() + { + return $this->strategy; + } + + /** + * Returns whether this ACE is granting, or denying + * + * @return bool + */ + public function isGranting() + { + return $this->isGranting; + } + + /** + * Whether auditing for successful grants is turned on + * + * @return bool + */ + public function isAuditFailure() + { + return $this->auditFailure; + } + + /** + * Whether auditing for successful denies is turned on + * + * @return bool + */ + public function isAuditSuccess() + { + return $this->auditSuccess; + } +} diff --git a/Security/Acl/Domain/FieldEntry.php b/Security/Acl/Domain/FieldEntry.php new file mode 100644 index 0000000..89e517d --- /dev/null +++ b/Security/Acl/Domain/FieldEntry.php @@ -0,0 +1,101 @@ + + */ +class FieldEntry extends Entry implements FieldEntryInterface +{ + protected $field; + + /** + * Constructor. + * + * @param \Propel\PropelBundle\Model\Acl\Entry $entry + * @param \Symfony\Component\Security\Acl\Model\AclInterface $acl + */ + public function __construct(ModelEntry $entry, AclInterface $acl) + { + $this->field = $entry->getFieldName(); + + parent::__construct($entry, $acl); + } + + /** + * Returns the field used for this entry. + * + * @return string + */ + public function getField() + { + return $this->field; + } + + /** + * String representation of object + * + * @link http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or &null; + */ + public function serialize() + { + return serialize(array( + $this->acl, + $this->securityIdentity, + $this->id, + $this->mask, + $this->isGranting, + $this->strategy, + $this->auditFailure, + $this->auditSuccess, + $this->field, + )); + } + + /** + * Constructs the object + * + * @link http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized + * + * @return mixed the original value unserialized. + */ + public function unserialize($serialized) + { + list( + $this->acl, + $this->securityIdentity, + $this->id, + $this->mask, + $this->isGranting, + $this->strategy, + $this->auditFailure, + $this->auditSuccess, + $this->field, + ) = unserialize($serialized); + + return $this; + } +} diff --git a/Security/Acl/Domain/MutableAcl.php b/Security/Acl/Domain/MutableAcl.php new file mode 100644 index 0000000..49b3342 --- /dev/null +++ b/Security/Acl/Domain/MutableAcl.php @@ -0,0 +1,531 @@ + + */ +class MutableAcl extends Acl implements MutableAclInterface +{ + /** + * The id of the current ACL. + * + * It's the id of the ObjectIdentity model. + * + * @var int + */ + protected $id; + + /** + * A reference to the ObjectIdentity this ACL is mapped to. + * + * @var \Propel\PropelBundle\Model\Acl\ObjectIdentity + */ + protected $modelObjectIdentity; + + /** + * A connection to be used for all changes on the ACL. + * + * @var ConnectionInterface + */ + protected $con; + + /** + * Constructor. + * + * @param ObjectCollection $entries + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * @param \Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface $permissionGrantingStrategy + * @param array $loadedSecurityIdentities + * @param \Symfony\Component\Security\Acl\Model\AclInterface $parentAcl + * @param bool $inherited + * @param ConnectionInterface $con + */ + public function __construct(ObjectCollection $entries, ObjectIdentityInterface $objectIdentity, PermissionGrantingStrategyInterface $permissionGrantingStrategy, array $loadedSecurityIdentities = array(), AclInterface $parentAcl = null, $inherited = true, ConnectionInterface $con = null) + { + parent::__construct($entries, $objectIdentity, $permissionGrantingStrategy, $loadedSecurityIdentities, $parentAcl, $inherited); + + $this->modelObjectIdentity = ObjectIdentityQuery::create() + ->filterByAclObjectIdentity($objectIdentity, $con) + ->findOneOrCreate($con) + ; + + if ($this->modelObjectIdentity->isNew()) { + $this->modelObjectIdentity->save($con); + } + + $this->id = $this->modelObjectIdentity->getId(); + + $this->con = $con; + } + + /** + * Returns the primary key of this ACL + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Sets whether entries are inherited + * + * @param bool $boolean + */ + public function setEntriesInheriting($boolean) + { + $this->inherited = $boolean; + } + + /** + * Sets the parent ACL + * + * @param \Symfony\Component\Security\Acl\Model\AclInterface|null $acl + */ + public function setParentAcl(AclInterface $acl = null) + { + $this->parentAcl = $acl; + } + + /** + * Deletes a class-based ACE + * + * @param integer $index + */ + public function deleteClassAce($index) + { + $this->deleteIndex($this->classAces, $index); + } + + /** + * Deletes a class-field-based ACE + * + * @param integer $index + * @param string $field + */ + public function deleteClassFieldAce($index, $field) + { + $this + ->validateField($this->classFieldAces, $field) + ->deleteIndex($this->classFieldAces[$field], $index) + ; + } + + /** + * Deletes an object-based ACE + * + * @param integer $index + */ + public function deleteObjectAce($index) + { + $this->deleteIndex($this->objectAces, $index); + } + + /** + * Deletes an object-field-based ACE + * + * @param integer $index + * @param string $field + */ + public function deleteObjectFieldAce($index, $field) + { + $this + ->validateField($this->objectFieldAces, $field) + ->deleteIndex($this->objectFieldAces[$field], $index) + ; + } + + /** + * Inserts a class-based ACE + * + * @param \Symfony\Component\Security\Acl\Model\SecurityIdentityInterface $securityIdentity + * @param integer $mask + * @param integer $index + * @param bool $granting + * @param string $strategy + */ + public function insertClassAce(SecurityIdentityInterface $securityIdentity, $mask, $index = 0, $granting = true, $strategy = null) + { + $this->insertToList($this->classAces, $index, $this->createAce($mask, $index, $securityIdentity, $strategy, $granting)); + } + + /** + * Inserts a class-field-based ACE + * + * @param string $field + * @param \Symfony\Component\Security\Acl\Model\SecurityIdentityInterface $securityIdentity + * @param integer $mask + * @param integer $index + * @param boolean $granting + * @param string $strategy + */ + public function insertClassFieldAce($field, SecurityIdentityInterface $securityIdentity, $mask, $index = 0, $granting = true, $strategy = null) + { + if (!isset($this->classFieldAces[$field])) { + $this->classFieldAces[$field] = array(); + } + + $this->insertToList($this->classFieldAces[$field], $index, $this->createAce($mask, $index, $securityIdentity, $strategy, $granting, $field)); + } + + /** + * Inserts an object-based ACE + * + * @param \Symfony\Component\Security\Acl\Model\SecurityIdentityInterface $securityIdentity + * @param integer $mask + * @param integer $index + * @param boolean $granting + * @param string $strategy + */ + public function insertObjectAce(SecurityIdentityInterface $securityIdentity, $mask, $index = 0, $granting = true, $strategy = null) + { + $this->insertToList($this->objectAces, $index, $this->createAce($mask, $index, $securityIdentity, $strategy, $granting)); + } + + /** + * Inserts an object-field-based ACE + * + * @param string $field + * @param \Symfony\Component\Security\Acl\Model\SecurityIdentityInterface $securityIdentity + * @param integer $mask + * @param integer $index + * @param boolean $granting + * @param string $strategy + */ + public function insertObjectFieldAce($field, SecurityIdentityInterface $securityIdentity, $mask, $index = 0, $granting = true, $strategy = null) + { + if (!isset($this->objectFieldAces[$field])) { + $this->objectFieldAces[$field] = array(); + } + + $this->insertToList($this->objectFieldAces[$field], $index, $this->createAce($mask, $index, $securityIdentity, $strategy, $granting, $field)); + } + + /** + * Updates a class-based ACE + * + * @param integer $index + * @param integer $mask + * @param string $strategy if null the strategy should not be changed + */ + public function updateClassAce($index, $mask, $strategy = null) + { + $this->updateAce($this->classAces, $index, $mask, $strategy); + } + + /** + * 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 + */ + public function updateClassFieldAce($index, $field, $mask, $strategy = null) + { + $this + ->validateField($this->classFieldAces, $field) + ->updateAce($this->classFieldAces[$field], $index, $mask, $strategy) + ; + } + + /** + * Updates an object-based ACE + * + * @param integer $index + * @param integer $mask + * @param string $strategy if null the strategy should not be changed + */ + public function updateObjectAce($index, $mask, $strategy = null) + { + $this->updateAce($this->objectAces, $index, $mask, $strategy); + } + + /** + * 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 + */ + public function updateObjectFieldAce($index, $field, $mask, $strategy = null) + { + $this->validateField($this->objectFieldAces, $field); + $this->updateAce($this->objectFieldAces[$field], $index, $mask, $strategy); + } + + /** + * String representation of object + * + * @link http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or &null; + */ + public function serialize() + { + return serialize(array( + $this->id, + $this->modelObjectIdentity, + $this->model, + $this->classAces, + $this->classFieldAces, + $this->objectAces, + $this->objectFieldAces, + $this->objectIdentity, + $this->parentAcl, + $this->permissionGrantingStrategy, + $this->inherited, + $this->loadedSecurityIdentities, + )); + } + + /** + * Constructs the object + * + * @link http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized + * + * @return mixed the original value unserialized. + */ + public function unserialize($serialized) + { + list( + $this->id, + $this->modelObjectIdentity, + $this->model, + $this->classAces, + $this->classFieldAces, + $this->objectAces, + $this->objectFieldAces, + $this->objectIdentity, + $this->parentAcl, + $this->permissionGrantingStrategy, + $this->inherited, + $this->loadedSecurityIdentities, + ) = unserialize($serialized); + + return $this; + } + + /** + * Insert a given entry into the list on the given index by shifting all others. + * + * @param array $list + * @param int $index + * @param \Propel\PropelBundle\Model\Acl\Entry\Entry $entry + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl $this + */ + protected function insertToList(array &$list, $index, Entry $entry) + { + $this->isWithinBounds($list, $index); + + if ($entry instanceof FieldEntry) { + $this->updateFields($entry->getField()); + } + + $list = array_merge( + array_slice($list, 0, $index), + array($entry), + array_splice($list, $index) + ); + + return $this; + } + + /** + * Update a single ACE of this ACL. + * + * @param array $list + * @param int $index + * @param int $mask + * @param string $strategy + * @param string $field + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl $this + */ + protected function updateAce(array &$list, $index, $mask, $strategy = null) + { + $this->validateIndex($list, $index); + + $entry = ModelEntry::fromAclEntry($list[$index]); + + // Apply updates + $entry->setMask($mask); + if (null !== $strategy) { + $entry->setGrantingStrategy($strategy); + } + + $list[$index] = ModelEntry::toAclEntry($entry, $this); + + return $this; + } + + /** + * Delete the ACE of the given list and index. + * + * The list will be re-ordered to have a valid 0..x list. + * + * @param array $list + * @param $index + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl $this + */ + protected function deleteIndex(array &$list, $index) + { + $this->validateIndex($list, $index); + unset($list[$index]); + $this->reorderList($list, $index-1); + + return $this; + } + + /** + * Validate the index on the given list of ACEs. + * + * @throws \OutOfBoundsException + * + * @param array $list + * @param int $index + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl $this + */ + protected function isWithinBounds(array &$list, $index) + { + // No count()-1, the count is one ahead of index, and could create the next valid entry! + if ($index < 0 or $index > count($list)) { + throw new \OutOfBoundsException(sprintf('The index must be in the interval [0, %d].', count($list))); + } + + return $this; + } + + /** + * Check the index for existence in the given list. + * + * @throws \OutOfBoundsException + * + * @param array $list + * @param $index + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl $this + */ + protected function validateIndex(array &$list, $index) + { + if (!isset($list[$index])) { + throw new \OutOfBoundsException(sprintf('The index "%d" does not exist.', $index)); + } + + return $this; + } + + /** + * Validate the given field to be present. + * + * @throws \InvalidArgumentException + * + * @param array $list + * @param string $field + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl $this + */ + protected function validateField(array &$list, $field) + { + if (!isset($list[$field])) { + throw new \InvalidArgumentException(sprintf('The given field "%s" does not exist.', $field)); + } + + return $this; + } + + /** + * Order the given list to have numeric indexes from 0..x + * + * @param array $list + * @param int $index The right boundary to which the list is valid. + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl $this + */ + protected function reorderList(array &$list, $index) + { + $list = array_merge( + array_slice($list, 0, $index+1), // +1 to get length + array_splice($list, $index+1) // +1 to get first index to re-order + ); + + return $this; + } + + /** + * Create a new ACL Entry. + * + * @param int $mask + * @param int $index + * @param \Symfony\Component\Security\Acl\Model\SecurityIdentityInterface $securityIdentity + * @param string $strategy + * @param bool $granting + * @param string $field + * + * @return \Propel\PropelBundle\Security\Acl\Domain\Entry|\Propel\PropelBundle\Security\Acl\Domain\FieldEntry + */ + protected function createAce($mask, $index, SecurityIdentityInterface $securityIdentity, $strategy = null, $granting = true, $field = null) + { + if (!is_int($mask)) { + throw new \InvalidArgumentException('The given mask is not valid. Please provide an integer.'); + } + + // Compatibility with default implementation + if (null === $strategy) { + if (true === $granting) { + $strategy = PermissionGrantingStrategy::ALL; + } else { + $strategy = PermissionGrantingStrategy::ANY; + } + } + + $model = new ModelEntry(); + $model + ->setAceOrder($index) + ->setMask($mask) + ->setGrantingStrategy($strategy) + ->setGranting($granting) + ->setSecurityIdentity(SecurityIdentity::fromAclIdentity($securityIdentity)) + ; + + if (null !== $field) { + $model->setFieldName($field); + + return new FieldEntry($model, $this); + } + + return new Entry($model, $this); + } +} diff --git a/Security/Acl/MutableAclProvider.php b/Security/Acl/MutableAclProvider.php new file mode 100644 index 0000000..651e0ed --- /dev/null +++ b/Security/Acl/MutableAclProvider.php @@ -0,0 +1,340 @@ + + */ +class MutableAclProvider extends AclProvider implements MutableAclProviderInterface +{ + /** + * Constructor. + * + * @param \Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface $permissionGrantingStrategy + * @param ConnectionInterface $connection + * @param \Symfony\Component\Security\Acl\Model\AclCacheInterface $cache + */ + public function __construct(PermissionGrantingStrategyInterface $permissionGrantingStrategy, ConnectionInterface $connection = null, AclCacheInterface $cache = null) + { + // @codeCoverageIgnoreStart + if (null === $connection) { + $connection = Propel::getConnection(EntryTableMap::DATABASE_NAME, ServiceContainerInterface::CONNECTION_WRITE); + } + // @codeCoverageIgnoreEnd + + parent::__construct($permissionGrantingStrategy, $connection, $cache); + } + + /** + * Creates a new ACL for the given object identity. + * + * @throws \Symfony\Component\Security\Acl\Exception\AclAlreadyExistsException When there already is an ACL for the given object identity. + * + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl + */ + public function createAcl(ObjectIdentityInterface $objectIdentity) + { + $entries = EntryQuery::create()->findByAclIdentity($objectIdentity, array(), $this->connection); + if (count($entries)) { + throw new AclAlreadyExistsException('An ACL for the given object identity already exists, find and update that one.'); + } + + $objIdentity = ObjectIdentityQuery::create() + ->filterByAclObjectIdentity($objectIdentity, $this->connection) + ->findOneOrCreate($this->connection) + ; + + if ($objIdentity->isNew()) { + // This is safe to do, it makes the ID available and does not affect changes to any ACL. + $objIdentity->save($this->connection); + } + + return $this->getAcl($entries, $objectIdentity, array(), null, false); + } + + /** + * 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. + * + * @throws \Symfony\Component\Security\Acl\Exception\Exception + * + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * + * @return bool + */ + public function deleteAcl(ObjectIdentityInterface $objectIdentity) + { + try { + $objIdentity = ObjectIdentityQuery::create()->findOneByAclObjectIdentity($objectIdentity, $this->connection); + if (null === $objIdentity) { + // No object identity, no ACL, so deletion is successful (expected result is given). + return true; + } + + $this->connection->beginTransaction(); + + // Retrieve all class and class-field ACEs, if any. + $aces = EntryQuery::create()->findByAclIdentity($objectIdentity, array(), $this->connection); + if (count($aces)) { + // In case this is the last of its kind, delete the class and class-field ACEs. + $count = ObjectIdentityQuery::create()->filterByClassId($objIdentity->getClassId())->count($this->connection); + if (1 === $count) { + $aces->delete($this->connection); + } + } + + /* + * If caching is enabled, retrieve the (grand-)children of this ACL. + * Those will be removed from the cache as well, as their parents do not exist anymore. + */ + if (null !== $this->cache) { + $children = ObjectIdentityQuery::create()->findGrandChildren($objIdentity, $this->connection); + } + + // This deletes all object and object-field ACEs, too. + $objIdentity->delete($this->connection); + + $this->connection->commit(); + + if (null !== $this->cache) { + $this->cache->evictFromCacheById($objIdentity->getId()); + foreach ($children as $eachChild) { + $this->cache->evictFromCacheById($eachChild->getId()); + } + } + + return true; + // @codeCoverageIgnoreStart + } catch (Exception $e) { + throw new AclException('An error occurred while deleting the ACL.', 1, $e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Persists any changes which were made to the ACL, or any associated access control entries. + * + * Changes to parent ACLs are not persisted. + * + * @throws \Symfony\Component\Security\Acl\Exception\Exception + * + * @param \Symfony\Component\Security\Acl\Model\MutableAclInterface $acl + * + * @return bool + */ + public function updateAcl(MutableAclInterface $acl) + { + if (!$acl instanceof MutableAcl) { + throw new \InvalidArgumentException('The given ACL is not tracked by this provider. Please provide \Propel\PropelBundle\Security\Acl\Domain\MutableAcl only.'); + } + + try { + $modelEntries = EntryQuery::create()->findByAclIdentity($acl->getObjectIdentity(), array(), $this->connection); + $objectIdentity = ObjectIdentityQuery::create()->findOneByAclObjectIdentity($acl->getObjectIdentity(), $this->connection); + + $this->connection->beginTransaction(); + + $keepEntries = array_merge( + $this->persistAcl($acl->getClassAces(), $objectIdentity), + $this->persistAcl($acl->getObjectAces(), $objectIdentity, true) + ); + + foreach ($acl->getFields() as $eachField) { + $keepEntries = array_merge($keepEntries, + $this->persistAcl($acl->getClassFieldAces($eachField), $objectIdentity), + $this->persistAcl($acl->getObjectFieldAces($eachField), $objectIdentity, true) + ); + } + + foreach ($modelEntries as &$eachEntry) { + if (!in_array($eachEntry->getId(), $keepEntries)) { + $eachEntry->delete($this->connection); + } + } + + if (null === $acl->getParentAcl()) { + $objectIdentity + ->setParentObjectIdentityId(null) + ->save($this->connection) + ; + } else { + $objectIdentity + ->setParentObjectIdentityId($acl->getParentAcl()->getId()) + ->save($this->connection) + ; + } + + $this->connection->commit(); + + // After successfully committing the transaction, we are good to update the cache. + if (null !== $this->cache) { + $this->cache->evictFromCacheById($objectIdentity->getId()); + $this->cache->putInCache($acl); + } + + return true; + // @codeCoverageIgnoreStart + } catch (Exception $e) { + $this->connection->rollBack(); + + throw new AclException('An error occurred while updating the ACL.', 0, $e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Persist the given ACEs. + * + * @param array $accessControlEntries + * @param \Propel\PropelBundle\Model\Acl\ObjectIdentity $objectIdentity + * @param bool $object + * + * @return array The IDs of the persisted ACEs. + */ + protected function persistAcl(array $accessControlEntries, ObjectIdentity $objectIdentity, $object = false) + { + $entries = array(); + + /* @var $eachAce \Symfony\Component\Security\Acl\Model\EntryInterface */ + foreach ($accessControlEntries as $order => $eachAce) { + // If the given ACE has never been persisted, create a new one. + if (null === $entry = $this->getPersistedAce($eachAce, $objectIdentity, $object)) { + $entry = ModelEntry::fromAclEntry($eachAce); + } + + if (in_array($entry->getId(), $entries)) { + $entry = ModelEntry::fromAclEntry($eachAce); + } + + // Apply possible changes from local ACE. + $entry + ->setAceOrder($order) + ->setAclClass($objectIdentity->getAclClass()) + ->setMask($eachAce->getMask()) + ; + + if ($eachAce instanceof AuditableEntryInterface) { + if (is_bool($eachAce->isAuditSuccess())) { + $entry->setAuditSuccess($eachAce->isAuditSuccess()); + } + + if (is_bool($eachAce->isAuditFailure())) { + $entry->setAuditFailure($eachAce->isAuditFailure()); + } + } + + if (true === $object) { + $entry->setObjectIdentity($objectIdentity); + } + + $entry->save($this->connection); + + $entries[] = $entry->getId(); + } + + return $entries; + } + + /** + * Retrieve the persisted model for the given ACE. + * + * If none is given, null is returned. + * + * @param \Symfony\Component\Security\Acl\Model\EntryInterface $ace + * + * @return \Propel\PropelBundle\Model\Acl\Entry|null + */ + protected function getPersistedAce(EntryInterface $ace, ObjectIdentity $objectIdentity, $object = false) + { + if (null !== $ace->getId() and null !== $entry = EntryQuery::create()->findPk($ace->getId(), $this->connection)) { + $entry->reload(true, $this->connection); + + return $entry; + } + + /* + * The id is not set, but there may be an ACE in the database. + * + * This happens if the ACL has created new ACEs, but was not reloaded. + * We try to retrieve one by the unique key. + */ + $ukQuery = EntryQuery::create() + ->filterByAclClass($objectIdentity->getAclClass($this->connection)) + ->filterBySecurityIdentity(SecurityIdentity::fromAclIdentity($ace->getSecurityIdentity(), $this->connection)) + ; + + if (true === $object) { + $ukQuery->filterByObjectIdentity($objectIdentity); + } else { + $ukQuery->filterByObjectIdentityId(null, Criteria::ISNULL); + } + + if ($ace instanceof FieldEntryInterface) { + $ukQuery->filterByFieldName($ace->getField()); + } else { + $ukQuery->filterByFieldName(null, Criteria::ISNULL); + } + + return $ukQuery->findOne($this->connection); + } + + /** + * Get an ACL for this provider. + * + * @param ObjectCollection $collection + * @param \Symfony\Component\Security\Acl\Model\ObjectIdentityInterface $objectIdentity + * @param array $loadedSecurityIdentities + * @param \Symfony\Component\Security\Acl\Model\AclInterface $parentAcl + * @param bool $inherited + * + * @return \Propel\PropelBundle\Security\Acl\Domain\MutableAcl + */ + protected function getAcl(ObjectCollection $collection, ObjectIdentityInterface $objectIdentity, array $loadedSecurityIdentities = array(), AclInterface $parentAcl = null, $inherited = true) + { + return new MutableAcl($collection, $objectIdentity, $this->permissionGrantingStrategy, $loadedSecurityIdentities, $parentAcl, $inherited, $this->connection); + } +} diff --git a/Security/User/PropelUserProvider.php b/Security/User/PropelUserProvider.php new file mode 100644 index 0000000..08afe7a --- /dev/null +++ b/Security/User/PropelUserProvider.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Security\User; + +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; + +/** + * Provides easy to use provisioning for Propel model users. + * + * @author William DURAND + */ +class PropelUserProvider implements UserProviderInterface +{ + /** + * A Model class name. + * + * @var string + */ + protected $class; + + /** + * A Query class name. + * + * @var string + */ + protected $queryClass; + + /** + * A property to use to retrieve the user. + * + * @var string + */ + protected $property; + + /** + * Default constructor + * + * @param string $class The User model class. + * @param string|null $property The property to use to retrieve a user. + */ + public function __construct($class, $property = null) + { + $this->class = $class; + $this->queryClass = $class.'Query'; + $this->property = $property; + } + + /** + * {@inheritdoc} + */ + public function loadUserByUsername($username) + { + $queryClass = $this->queryClass; + $query = $queryClass::create(); + + if (null !== $this->property) { + $filter = 'filterBy'.ucfirst($this->property); + $query->$filter($username); + } else { + $query->filterByUsername($username); + } + + if (null === $user = $query->findOne()) { + throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); + } + + return $user; + } + + /** + * {@inheritdoc} + */ + public function refreshUser(UserInterface $user) + { + if (!$user instanceof $this->class) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); + } + + $queryClass = $this->queryClass; + + return $queryClass::create()->findPk($user->getPrimaryKey()); + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + return $class === $this->class; + } +} diff --git a/Tests/Fixtures/Model/User.php b/Tests/Fixtures/Model/User.php new file mode 100644 index 0000000..a9bfe4b --- /dev/null +++ b/Tests/Fixtures/Model/User.php @@ -0,0 +1,13 @@ + + */ +class PropelUserProviderTest extends TestCase +{ + public function setUp() + { + $schema = << + + + + + + + + +
+ +SCHEMA; + + $builder = new QuickBuilder(); + $builder->setSchema($schema); + $classTargets = null; + + $this->con = $builder->build($dsn = null, $user = null, $pass = null, $adapter = null, $classTargets); + } + + public function testRefreshUserGetsUserByPrimaryKey() + { + $user1 = new User(); + $user1->setUsername('user1'); + $user1->save(); + + $user2 = new User(); + $user2->setUsername('user2'); + $user2->save(); + + $provider = new PropelUserProvider('Propel\PropelBundle\Tests\Fixtures\Model\User', 'username'); + + // try to change the user identity + $user1->setUsername('user2'); + + $resultUser = $provider->refreshUser($user1); + $this->assertSame($user1, $resultUser); + } +}