MutableAclProvider.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Security\Acl\Dbal;
  11. use Doctrine\Common\PropertyChangedListener;
  12. use Doctrine\DBAL\Driver\Connection;
  13. use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
  14. use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
  15. use Symfony\Component\Security\Acl\Exception\AclAlreadyExistsException;
  16. use Symfony\Component\Security\Acl\Exception\ConcurrentModificationException;
  17. use Symfony\Component\Security\Acl\Model\AclCacheInterface;
  18. use Symfony\Component\Security\Acl\Model\AclInterface;
  19. use Symfony\Component\Security\Acl\Model\EntryInterface;
  20. use Symfony\Component\Security\Acl\Model\MutableAclInterface;
  21. use Symfony\Component\Security\Acl\Model\MutableAclProviderInterface;
  22. use Symfony\Component\Security\Acl\Model\ObjectIdentityInterface;
  23. use Symfony\Component\Security\Acl\Model\PermissionGrantingStrategyInterface;
  24. use Symfony\Component\Security\Acl\Model\SecurityIdentityInterface;
  25. /**
  26. * An implementation of the MutableAclProviderInterface using Doctrine DBAL.
  27. *
  28. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  29. */
  30. class MutableAclProvider extends AclProvider implements MutableAclProviderInterface, PropertyChangedListener
  31. {
  32. private $propertyChanges;
  33. /**
  34. * {@inheritDoc}
  35. */
  36. public function __construct(Connection $connection, PermissionGrantingStrategyInterface $permissionGrantingStrategy, array $options, AclCacheInterface $cache = null)
  37. {
  38. parent::__construct($connection, $permissionGrantingStrategy, $options, $cache);
  39. $this->propertyChanges = new \SplObjectStorage();
  40. }
  41. /**
  42. * {@inheritDoc}
  43. */
  44. public function createAcl(ObjectIdentityInterface $oid)
  45. {
  46. if (false !== $this->retrieveObjectIdentityPrimaryKey($oid)) {
  47. throw new AclAlreadyExistsException(sprintf('%s is already associated with an ACL.', $oid));
  48. }
  49. $this->connection->beginTransaction();
  50. try {
  51. $this->createObjectIdentity($oid);
  52. $pk = $this->retrieveObjectIdentityPrimaryKey($oid);
  53. $this->connection->executeQuery($this->getInsertObjectIdentityRelationSql($pk, $pk));
  54. $this->connection->commit();
  55. } catch (\Exception $failed) {
  56. $this->connection->rollBack();
  57. throw $failed;
  58. }
  59. // re-read the ACL from the database to ensure proper caching, etc.
  60. return $this->findAcl($oid);
  61. }
  62. /**
  63. * {@inheritDoc}
  64. */
  65. public function deleteAcl(ObjectIdentityInterface $oid)
  66. {
  67. $this->connection->beginTransaction();
  68. try {
  69. foreach ($this->findChildren($oid, true) as $childOid) {
  70. $this->deleteAcl($childOid);
  71. }
  72. $oidPK = $this->retrieveObjectIdentityPrimaryKey($oid);
  73. $this->deleteAccessControlEntries($oidPK);
  74. $this->deleteObjectIdentityRelations($oidPK);
  75. $this->deleteObjectIdentity($oidPK);
  76. $this->connection->commit();
  77. } catch (\Exception $failed) {
  78. $this->connection->rollBack();
  79. throw $failed;
  80. }
  81. // evict the ACL from the in-memory identity map
  82. if (isset($this->loadedAcls[$oid->getType()][$oid->getIdentifier()])) {
  83. $this->propertyChanges->offsetUnset($this->loadedAcls[$oid->getType()][$oid->getIdentifier()]);
  84. unset($this->loadedAcls[$oid->getType()][$oid->getIdentifier()]);
  85. }
  86. // evict the ACL from any caches
  87. if (null !== $this->cache) {
  88. $this->cache->evictFromCacheByIdentity($oid);
  89. }
  90. }
  91. /**
  92. * {@inheritDoc}
  93. */
  94. public function findAcls(array $oids, array $sids = array())
  95. {
  96. $result = parent::findAcls($oids, $sids);
  97. foreach ($result as $oid) {
  98. $acl = $result->offsetGet($oid);
  99. if (false === $this->propertyChanges->contains($acl) && $acl instanceof MutableAclInterface) {
  100. $acl->addPropertyChangedListener($this);
  101. $this->propertyChanges->attach($acl, array());
  102. }
  103. $parentAcl = $acl->getParentAcl();
  104. while (null !== $parentAcl) {
  105. if (false === $this->propertyChanges->contains($parentAcl) && $acl instanceof MutableAclInterface) {
  106. $parentAcl->addPropertyChangedListener($this);
  107. $this->propertyChanges->attach($parentAcl, array());
  108. }
  109. $parentAcl = $parentAcl->getParentAcl();
  110. }
  111. }
  112. return $result;
  113. }
  114. /**
  115. * Implementation of PropertyChangedListener
  116. *
  117. * This allows us to keep track of which values have been changed, so we don't
  118. * have to do a full introspection when ->updateAcl() is called.
  119. *
  120. * @param mixed $sender
  121. * @param string $propertyName
  122. * @param mixed $oldValue
  123. * @param mixed $newValue
  124. *
  125. * @throws \InvalidArgumentException
  126. */
  127. public function propertyChanged($sender, $propertyName, $oldValue, $newValue)
  128. {
  129. if (!$sender instanceof MutableAclInterface && !$sender instanceof EntryInterface) {
  130. throw new \InvalidArgumentException('$sender must be an instance of MutableAclInterface, or EntryInterface.');
  131. }
  132. if ($sender instanceof EntryInterface) {
  133. if (null === $sender->getId()) {
  134. return;
  135. }
  136. $ace = $sender;
  137. $sender = $ace->getAcl();
  138. } else {
  139. $ace = null;
  140. }
  141. if (false === $this->propertyChanges->contains($sender)) {
  142. throw new \InvalidArgumentException('$sender is not being tracked by this provider.');
  143. }
  144. $propertyChanges = $this->propertyChanges->offsetGet($sender);
  145. if (null === $ace) {
  146. if (isset($propertyChanges[$propertyName])) {
  147. $oldValue = $propertyChanges[$propertyName][0];
  148. if ($oldValue === $newValue) {
  149. unset($propertyChanges[$propertyName]);
  150. } else {
  151. $propertyChanges[$propertyName] = array($oldValue, $newValue);
  152. }
  153. } else {
  154. $propertyChanges[$propertyName] = array($oldValue, $newValue);
  155. }
  156. } else {
  157. if (!isset($propertyChanges['aces'])) {
  158. $propertyChanges['aces'] = new \SplObjectStorage();
  159. }
  160. $acePropertyChanges = $propertyChanges['aces']->contains($ace)? $propertyChanges['aces']->offsetGet($ace) : array();
  161. if (isset($acePropertyChanges[$propertyName])) {
  162. $oldValue = $acePropertyChanges[$propertyName][0];
  163. if ($oldValue === $newValue) {
  164. unset($acePropertyChanges[$propertyName]);
  165. } else {
  166. $acePropertyChanges[$propertyName] = array($oldValue, $newValue);
  167. }
  168. } else {
  169. $acePropertyChanges[$propertyName] = array($oldValue, $newValue);
  170. }
  171. if (count($acePropertyChanges) > 0) {
  172. $propertyChanges['aces']->offsetSet($ace, $acePropertyChanges);
  173. } else {
  174. $propertyChanges['aces']->offsetUnset($ace);
  175. if (0 === count($propertyChanges['aces'])) {
  176. unset($propertyChanges['aces']);
  177. }
  178. }
  179. }
  180. $this->propertyChanges->offsetSet($sender, $propertyChanges);
  181. }
  182. /**
  183. * {@inheritDoc}
  184. */
  185. public function updateAcl(MutableAclInterface $acl)
  186. {
  187. if (!$this->propertyChanges->contains($acl)) {
  188. throw new \InvalidArgumentException('$acl is not tracked by this provider.');
  189. }
  190. $propertyChanges = $this->propertyChanges->offsetGet($acl);
  191. // check if any changes were made to this ACL
  192. if (0 === count($propertyChanges)) {
  193. return;
  194. }
  195. $sets = $sharedPropertyChanges = array();
  196. $this->connection->beginTransaction();
  197. try {
  198. if (isset($propertyChanges['entriesInheriting'])) {
  199. $sets[] = 'entries_inheriting = '.$this->connection->getDatabasePlatform()->convertBooleans($propertyChanges['entriesInheriting'][1]);
  200. }
  201. if (isset($propertyChanges['parentAcl'])) {
  202. if (null === $propertyChanges['parentAcl'][1]) {
  203. $sets[] = 'parent_object_identity_id = NULL';
  204. } else {
  205. $sets[] = 'parent_object_identity_id = '.intval($propertyChanges['parentAcl'][1]->getId());
  206. }
  207. $this->regenerateAncestorRelations($acl);
  208. $childAcls = $this->findAcls($this->findChildren($acl->getObjectIdentity(), false));
  209. foreach ($childAcls as $childOid) {
  210. $this->regenerateAncestorRelations($childAcls[$childOid]);
  211. }
  212. }
  213. // this includes only updates of existing ACEs, but neither the creation, nor
  214. // the deletion of ACEs; these are tracked by changes to the ACL's respective
  215. // properties (classAces, classFieldAces, objectAces, objectFieldAces)
  216. if (isset($propertyChanges['aces'])) {
  217. $this->updateAces($propertyChanges['aces']);
  218. }
  219. // check properties for deleted, and created ACEs
  220. if (isset($propertyChanges['classAces'])) {
  221. $this->updateAceProperty('classAces', $propertyChanges['classAces']);
  222. $sharedPropertyChanges['classAces'] = $propertyChanges['classAces'];
  223. }
  224. if (isset($propertyChanges['classFieldAces'])) {
  225. $this->updateFieldAceProperty('classFieldAces', $propertyChanges['classFieldAces']);
  226. $sharedPropertyChanges['classFieldAces'] = $propertyChanges['classFieldAces'];
  227. }
  228. if (isset($propertyChanges['objectAces'])) {
  229. $this->updateAceProperty('objectAces', $propertyChanges['objectAces']);
  230. }
  231. if (isset($propertyChanges['objectFieldAces'])) {
  232. $this->updateFieldAceProperty('objectFieldAces', $propertyChanges['objectFieldAces']);
  233. }
  234. // if there have been changes to shared properties, we need to synchronize other
  235. // ACL instances for object identities of the same type that are already in-memory
  236. if (count($sharedPropertyChanges) > 0) {
  237. $classAcesProperty = new \ReflectionProperty('Symfony\Component\Security\Acl\Domain\Acl', 'classAces');
  238. $classAcesProperty->setAccessible(true);
  239. $classFieldAcesProperty = new \ReflectionProperty('Symfony\Component\Security\Acl\Domain\Acl', 'classFieldAces');
  240. $classFieldAcesProperty->setAccessible(true);
  241. foreach ($this->loadedAcls[$acl->getObjectIdentity()->getType()] as $sameTypeAcl) {
  242. if (isset($sharedPropertyChanges['classAces'])) {
  243. if ($acl !== $sameTypeAcl && $classAcesProperty->getValue($sameTypeAcl) !== $sharedPropertyChanges['classAces'][0]) {
  244. throw new ConcurrentModificationException('The "classAces" property has been modified concurrently.');
  245. }
  246. $classAcesProperty->setValue($sameTypeAcl, $sharedPropertyChanges['classAces'][1]);
  247. }
  248. if (isset($sharedPropertyChanges['classFieldAces'])) {
  249. if ($acl !== $sameTypeAcl && $classFieldAcesProperty->getValue($sameTypeAcl) !== $sharedPropertyChanges['classFieldAces'][0]) {
  250. throw new ConcurrentModificationException('The "classFieldAces" property has been modified concurrently.');
  251. }
  252. $classFieldAcesProperty->setValue($sameTypeAcl, $sharedPropertyChanges['classFieldAces'][1]);
  253. }
  254. }
  255. }
  256. // persist any changes to the acl_object_identities table
  257. if (count($sets) > 0) {
  258. $this->connection->executeQuery($this->getUpdateObjectIdentitySql($acl->getId(), $sets));
  259. }
  260. $this->connection->commit();
  261. } catch (\Exception $failed) {
  262. $this->connection->rollBack();
  263. throw $failed;
  264. }
  265. $this->propertyChanges->offsetSet($acl, array());
  266. if (null !== $this->cache) {
  267. if (count($sharedPropertyChanges) > 0) {
  268. // FIXME: Currently, there is no easy way to clear the cache for ACLs
  269. // of a certain type. The problem here is that we need to make
  270. // sure to clear the cache of all child ACLs as well, and these
  271. // child ACLs might be of a different class type.
  272. $this->cache->clearCache();
  273. } else {
  274. // if there are no shared property changes, it's sufficient to just delete
  275. // the cache for this ACL
  276. $this->cache->evictFromCacheByIdentity($acl->getObjectIdentity());
  277. foreach ($this->findChildren($acl->getObjectIdentity()) as $childOid) {
  278. $this->cache->evictFromCacheByIdentity($childOid);
  279. }
  280. }
  281. }
  282. }
  283. /**
  284. * Constructs the SQL for deleting access control entries.
  285. *
  286. * @param integer $oidPK
  287. * @return string
  288. */
  289. protected function getDeleteAccessControlEntriesSql($oidPK)
  290. {
  291. return sprintf(
  292. 'DELETE FROM %s WHERE object_identity_id = %d',
  293. $this->options['entry_table_name'],
  294. $oidPK
  295. );
  296. }
  297. /**
  298. * Constructs the SQL for deleting a specific ACE.
  299. *
  300. * @param integer $acePK
  301. * @return string
  302. */
  303. protected function getDeleteAccessControlEntrySql($acePK)
  304. {
  305. return sprintf(
  306. 'DELETE FROM %s WHERE id = %d',
  307. $this->options['entry_table_name'],
  308. $acePK
  309. );
  310. }
  311. /**
  312. * Constructs the SQL for deleting an object identity.
  313. *
  314. * @param integer $pk
  315. * @return string
  316. */
  317. protected function getDeleteObjectIdentitySql($pk)
  318. {
  319. return sprintf(
  320. 'DELETE FROM %s WHERE id = %d',
  321. $this->options['oid_table_name'],
  322. $pk
  323. );
  324. }
  325. /**
  326. * Constructs the SQL for deleting relation entries.
  327. *
  328. * @param integer $pk
  329. * @return string
  330. */
  331. protected function getDeleteObjectIdentityRelationsSql($pk)
  332. {
  333. return sprintf(
  334. 'DELETE FROM %s WHERE object_identity_id = %d',
  335. $this->options['oid_ancestors_table_name'],
  336. $pk
  337. );
  338. }
  339. /**
  340. * Constructs the SQL for inserting an ACE.
  341. *
  342. * @param integer $classId
  343. * @param integer|null $objectIdentityId
  344. * @param string|null $field
  345. * @param integer $aceOrder
  346. * @param integer $securityIdentityId
  347. * @param string $strategy
  348. * @param integer $mask
  349. * @param Boolean $granting
  350. * @param Boolean $auditSuccess
  351. * @param Boolean $auditFailure
  352. * @return string
  353. */
  354. protected function getInsertAccessControlEntrySql($classId, $objectIdentityId, $field, $aceOrder, $securityIdentityId, $strategy, $mask, $granting, $auditSuccess, $auditFailure)
  355. {
  356. $query = <<<QUERY
  357. INSERT INTO %s (
  358. class_id,
  359. object_identity_id,
  360. field_name,
  361. ace_order,
  362. security_identity_id,
  363. mask,
  364. granting,
  365. granting_strategy,
  366. audit_success,
  367. audit_failure
  368. )
  369. VALUES (%d, %s, %s, %d, %d, %d, %s, %s, %s, %s)
  370. QUERY;
  371. return sprintf(
  372. $query,
  373. $this->options['entry_table_name'],
  374. $classId,
  375. null === $objectIdentityId? 'NULL' : intval($objectIdentityId),
  376. null === $field? 'NULL' : $this->connection->quote($field),
  377. $aceOrder,
  378. $securityIdentityId,
  379. $mask,
  380. $this->connection->getDatabasePlatform()->convertBooleans($granting),
  381. $this->connection->quote($strategy),
  382. $this->connection->getDatabasePlatform()->convertBooleans($auditSuccess),
  383. $this->connection->getDatabasePlatform()->convertBooleans($auditFailure)
  384. );
  385. }
  386. /**
  387. * Constructs the SQL for inserting a new class type.
  388. *
  389. * @param string $classType
  390. * @return string
  391. */
  392. protected function getInsertClassSql($classType)
  393. {
  394. return sprintf(
  395. 'INSERT INTO %s (class_type) VALUES (%s)',
  396. $this->options['class_table_name'],
  397. $this->connection->quote($classType)
  398. );
  399. }
  400. /**
  401. * Constructs the SQL for inserting a relation entry.
  402. *
  403. * @param integer $objectIdentityId
  404. * @param integer $ancestorId
  405. * @return string
  406. */
  407. protected function getInsertObjectIdentityRelationSql($objectIdentityId, $ancestorId)
  408. {
  409. return sprintf(
  410. 'INSERT INTO %s (object_identity_id, ancestor_id) VALUES (%d, %d)',
  411. $this->options['oid_ancestors_table_name'],
  412. $objectIdentityId,
  413. $ancestorId
  414. );
  415. }
  416. /**
  417. * Constructs the SQL for inserting an object identity.
  418. *
  419. * @param string $identifier
  420. * @param integer $classId
  421. * @param Boolean $entriesInheriting
  422. * @return string
  423. */
  424. protected function getInsertObjectIdentitySql($identifier, $classId, $entriesInheriting)
  425. {
  426. $query = <<<QUERY
  427. INSERT INTO %s (class_id, object_identifier, entries_inheriting)
  428. VALUES (%d, %s, %s)
  429. QUERY;
  430. return sprintf(
  431. $query,
  432. $this->options['oid_table_name'],
  433. $classId,
  434. $this->connection->quote($identifier),
  435. $this->connection->getDatabasePlatform()->convertBooleans($entriesInheriting)
  436. );
  437. }
  438. /**
  439. * Constructs the SQL for inserting a security identity.
  440. *
  441. * @param SecurityIdentityInterface $sid
  442. * @throws \InvalidArgumentException
  443. * @return string
  444. */
  445. protected function getInsertSecurityIdentitySql(SecurityIdentityInterface $sid)
  446. {
  447. if ($sid instanceof UserSecurityIdentity) {
  448. $identifier = $sid->getClass().'-'.$sid->getUsername();
  449. $username = true;
  450. } elseif ($sid instanceof RoleSecurityIdentity) {
  451. $identifier = $sid->getRole();
  452. $username = false;
  453. } else {
  454. throw new \InvalidArgumentException('$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.');
  455. }
  456. return sprintf(
  457. 'INSERT INTO %s (identifier, username) VALUES (%s, %s)',
  458. $this->options['sid_table_name'],
  459. $this->connection->quote($identifier),
  460. $this->connection->getDatabasePlatform()->convertBooleans($username)
  461. );
  462. }
  463. /**
  464. * Constructs the SQL for selecting an ACE.
  465. *
  466. * @param integer $classId
  467. * @param integer $oid
  468. * @param string $field
  469. * @param integer $order
  470. * @return string
  471. */
  472. protected function getSelectAccessControlEntryIdSql($classId, $oid, $field, $order)
  473. {
  474. return sprintf(
  475. 'SELECT id FROM %s WHERE class_id = %d AND %s AND %s AND ace_order = %d',
  476. $this->options['entry_table_name'],
  477. $classId,
  478. null === $oid ?
  479. $this->connection->getDatabasePlatform()->getIsNullExpression('object_identity_id')
  480. : 'object_identity_id = '.intval($oid),
  481. null === $field ?
  482. $this->connection->getDatabasePlatform()->getIsNullExpression('field_name')
  483. : 'field_name = '.$this->connection->quote($field),
  484. $order
  485. );
  486. }
  487. /**
  488. * Constructs the SQL for selecting the primary key associated with
  489. * the passed class type.
  490. *
  491. * @param string $classType
  492. * @return string
  493. */
  494. protected function getSelectClassIdSql($classType)
  495. {
  496. return sprintf(
  497. 'SELECT id FROM %s WHERE class_type = %s',
  498. $this->options['class_table_name'],
  499. $this->connection->quote($classType)
  500. );
  501. }
  502. /**
  503. * Constructs the SQL for selecting the primary key of a security identity.
  504. *
  505. * @param SecurityIdentityInterface $sid
  506. * @throws \InvalidArgumentException
  507. * @return string
  508. */
  509. protected function getSelectSecurityIdentityIdSql(SecurityIdentityInterface $sid)
  510. {
  511. if ($sid instanceof UserSecurityIdentity) {
  512. $identifier = $sid->getClass().'-'.$sid->getUsername();
  513. $username = true;
  514. } elseif ($sid instanceof RoleSecurityIdentity) {
  515. $identifier = $sid->getRole();
  516. $username = false;
  517. } else {
  518. throw new \InvalidArgumentException('$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.');
  519. }
  520. return sprintf(
  521. 'SELECT id FROM %s WHERE identifier = %s AND username = %s',
  522. $this->options['sid_table_name'],
  523. $this->connection->quote($identifier),
  524. $this->connection->getDatabasePlatform()->convertBooleans($username)
  525. );
  526. }
  527. /**
  528. * Constructs the SQL for updating an object identity.
  529. *
  530. * @param integer $pk
  531. * @param array $changes
  532. * @throws \InvalidArgumentException
  533. * @return string
  534. */
  535. protected function getUpdateObjectIdentitySql($pk, array $changes)
  536. {
  537. if (0 === count($changes)) {
  538. throw new \InvalidArgumentException('There are no changes.');
  539. }
  540. return sprintf(
  541. 'UPDATE %s SET %s WHERE id = %d',
  542. $this->options['oid_table_name'],
  543. implode(', ', $changes),
  544. $pk
  545. );
  546. }
  547. /**
  548. * Constructs the SQL for updating an ACE.
  549. *
  550. * @param integer $pk
  551. * @param array $sets
  552. * @throws \InvalidArgumentException
  553. * @return string
  554. */
  555. protected function getUpdateAccessControlEntrySql($pk, array $sets)
  556. {
  557. if (0 === count($sets)) {
  558. throw new \InvalidArgumentException('There are no changes.');
  559. }
  560. return sprintf(
  561. 'UPDATE %s SET %s WHERE id = %d',
  562. $this->options['entry_table_name'],
  563. implode(', ', $sets),
  564. $pk
  565. );
  566. }
  567. /**
  568. * Creates the ACL for the passed object identity
  569. *
  570. * @param ObjectIdentityInterface $oid
  571. */
  572. private function createObjectIdentity(ObjectIdentityInterface $oid)
  573. {
  574. $classId = $this->createOrRetrieveClassId($oid->getType());
  575. $this->connection->executeQuery($this->getInsertObjectIdentitySql($oid->getIdentifier(), $classId, true));
  576. }
  577. /**
  578. * Returns the primary key for the passed class type.
  579. *
  580. * If the type does not yet exist in the database, it will be created.
  581. *
  582. * @param string $classType
  583. * @return integer
  584. */
  585. private function createOrRetrieveClassId($classType)
  586. {
  587. if (false !== $id = $this->connection->executeQuery($this->getSelectClassIdSql($classType))->fetchColumn()) {
  588. return $id;
  589. }
  590. $this->connection->executeQuery($this->getInsertClassSql($classType));
  591. return $this->connection->executeQuery($this->getSelectClassIdSql($classType))->fetchColumn();
  592. }
  593. /**
  594. * Returns the primary key for the passed security identity.
  595. *
  596. * If the security identity does not yet exist in the database, it will be
  597. * created.
  598. *
  599. * @param SecurityIdentityInterface $sid
  600. * @return integer
  601. */
  602. private function createOrRetrieveSecurityIdentityId(SecurityIdentityInterface $sid)
  603. {
  604. if (false !== $id = $this->connection->executeQuery($this->getSelectSecurityIdentityIdSql($sid))->fetchColumn()) {
  605. return $id;
  606. }
  607. $this->connection->executeQuery($this->getInsertSecurityIdentitySql($sid));
  608. return $this->connection->executeQuery($this->getSelectSecurityIdentityIdSql($sid))->fetchColumn();
  609. }
  610. /**
  611. * Deletes all ACEs for the given object identity primary key.
  612. *
  613. * @param integer $oidPK
  614. */
  615. private function deleteAccessControlEntries($oidPK)
  616. {
  617. $this->connection->executeQuery($this->getDeleteAccessControlEntriesSql($oidPK));
  618. }
  619. /**
  620. * Deletes the object identity from the database.
  621. *
  622. * @param integer $pk
  623. */
  624. private function deleteObjectIdentity($pk)
  625. {
  626. $this->connection->executeQuery($this->getDeleteObjectIdentitySql($pk));
  627. }
  628. /**
  629. * Deletes all entries from the relations table from the database.
  630. *
  631. * @param integer $pk
  632. */
  633. private function deleteObjectIdentityRelations($pk)
  634. {
  635. $this->connection->executeQuery($this->getDeleteObjectIdentityRelationsSql($pk));
  636. }
  637. /**
  638. * This regenerates the ancestor table which is used for fast read access.
  639. *
  640. * @param AclInterface $acl
  641. */
  642. private function regenerateAncestorRelations(AclInterface $acl)
  643. {
  644. $pk = $acl->getId();
  645. $this->connection->executeQuery($this->getDeleteObjectIdentityRelationsSql($pk));
  646. $this->connection->executeQuery($this->getInsertObjectIdentityRelationSql($pk, $pk));
  647. $parentAcl = $acl->getParentAcl();
  648. while (null !== $parentAcl) {
  649. $this->connection->executeQuery($this->getInsertObjectIdentityRelationSql($pk, $parentAcl->getId()));
  650. $parentAcl = $parentAcl->getParentAcl();
  651. }
  652. }
  653. /**
  654. * This processes changes on an ACE related property (classFieldAces, or objectFieldAces).
  655. *
  656. * @param string $name
  657. * @param array $changes
  658. */
  659. private function updateFieldAceProperty($name, array $changes)
  660. {
  661. $sids = new \SplObjectStorage();
  662. $classIds = new \SplObjectStorage();
  663. $currentIds = array();
  664. foreach ($changes[1] as $field => $new) {
  665. for ($i=0,$c=count($new); $i<$c; $i++) {
  666. $ace = $new[$i];
  667. if (null === $ace->getId()) {
  668. if ($sids->contains($ace->getSecurityIdentity())) {
  669. $sid = $sids->offsetGet($ace->getSecurityIdentity());
  670. } else {
  671. $sid = $this->createOrRetrieveSecurityIdentityId($ace->getSecurityIdentity());
  672. }
  673. $oid = $ace->getAcl()->getObjectIdentity();
  674. if ($classIds->contains($oid)) {
  675. $classId = $classIds->offsetGet($oid);
  676. } else {
  677. $classId = $this->createOrRetrieveClassId($oid->getType());
  678. }
  679. $objectIdentityId = $name === 'classFieldAces' ? null : $ace->getAcl()->getId();
  680. $this->connection->executeQuery($this->getInsertAccessControlEntrySql($classId, $objectIdentityId, $field, $i, $sid, $ace->getStrategy(), $ace->getMask(), $ace->isGranting(), $ace->isAuditSuccess(), $ace->isAuditFailure()));
  681. $aceId = $this->connection->executeQuery($this->getSelectAccessControlEntryIdSql($classId, $objectIdentityId, $field, $i))->fetchColumn();
  682. $this->loadedAces[$aceId] = $ace;
  683. $aceIdProperty = new \ReflectionProperty('Symfony\Component\Security\Acl\Domain\Entry', 'id');
  684. $aceIdProperty->setAccessible(true);
  685. $aceIdProperty->setValue($ace, intval($aceId));
  686. } else {
  687. $currentIds[$ace->getId()] = true;
  688. }
  689. }
  690. }
  691. foreach ($changes[0] as $old) {
  692. for ($i=0,$c=count($old); $i<$c; $i++) {
  693. $ace = $old[$i];
  694. if (!isset($currentIds[$ace->getId()])) {
  695. $this->connection->executeQuery($this->getDeleteAccessControlEntrySql($ace->getId()));
  696. unset($this->loadedAces[$ace->getId()]);
  697. }
  698. }
  699. }
  700. }
  701. /**
  702. * This processes changes on an ACE related property (classAces, or objectAces).
  703. *
  704. * @param string $name
  705. * @param array $changes
  706. */
  707. private function updateAceProperty($name, array $changes)
  708. {
  709. list($old, $new) = $changes;
  710. $sids = new \SplObjectStorage();
  711. $classIds = new \SplObjectStorage();
  712. $currentIds = array();
  713. for ($i=0,$c=count($new); $i<$c; $i++) {
  714. $ace = $new[$i];
  715. if (null === $ace->getId()) {
  716. if ($sids->contains($ace->getSecurityIdentity())) {
  717. $sid = $sids->offsetGet($ace->getSecurityIdentity());
  718. } else {
  719. $sid = $this->createOrRetrieveSecurityIdentityId($ace->getSecurityIdentity());
  720. }
  721. $oid = $ace->getAcl()->getObjectIdentity();
  722. if ($classIds->contains($oid)) {
  723. $classId = $classIds->offsetGet($oid);
  724. } else {
  725. $classId = $this->createOrRetrieveClassId($oid->getType());
  726. }
  727. $objectIdentityId = $name === 'classAces' ? null : $ace->getAcl()->getId();
  728. $this->connection->executeQuery($this->getInsertAccessControlEntrySql($classId, $objectIdentityId, null, $i, $sid, $ace->getStrategy(), $ace->getMask(), $ace->isGranting(), $ace->isAuditSuccess(), $ace->isAuditFailure()));
  729. $aceId = $this->connection->executeQuery($this->getSelectAccessControlEntryIdSql($classId, $objectIdentityId, null, $i))->fetchColumn();
  730. $this->loadedAces[$aceId] = $ace;
  731. $aceIdProperty = new \ReflectionProperty($ace, 'id');
  732. $aceIdProperty->setAccessible(true);
  733. $aceIdProperty->setValue($ace, intval($aceId));
  734. } else {
  735. $currentIds[$ace->getId()] = true;
  736. }
  737. }
  738. for ($i=0,$c=count($old); $i<$c; $i++) {
  739. $ace = $old[$i];
  740. if (!isset($currentIds[$ace->getId()])) {
  741. $this->connection->executeQuery($this->getDeleteAccessControlEntrySql($ace->getId()));
  742. unset($this->loadedAces[$ace->getId()]);
  743. }
  744. }
  745. }
  746. /**
  747. * Persists the changes which were made to ACEs to the database.
  748. *
  749. * @param \SplObjectStorage $aces
  750. */
  751. private function updateAces(\SplObjectStorage $aces)
  752. {
  753. foreach ($aces as $ace) {
  754. $propertyChanges = $aces->offsetGet($ace);
  755. $sets = array();
  756. if (isset($propertyChanges['mask'])) {
  757. $sets[] = sprintf('mask = %d', $propertyChanges['mask'][1]);
  758. }
  759. if (isset($propertyChanges['strategy'])) {
  760. $sets[] = sprintf('granting_strategy = %s', $this->connection->quote($propertyChanges['strategy']));
  761. }
  762. if (isset($propertyChanges['aceOrder'])) {
  763. $sets[] = sprintf('ace_order = %d', $propertyChanges['aceOrder'][1]);
  764. }
  765. if (isset($propertyChanges['auditSuccess'])) {
  766. $sets[] = sprintf('audit_success = %s', $this->connection->getDatabasePlatform()->convertBooleans($propertyChanges['auditSuccess'][1]));
  767. }
  768. if (isset($propertyChanges['auditFailure'])) {
  769. $sets[] = sprintf('audit_failure = %s', $this->connection->getDatabasePlatform()->convertBooleans($propertyChanges['auditFailure'][1]));
  770. }
  771. $this->connection->executeQuery($this->getUpdateAccessControlEntrySql($ace->getId(), $sets));
  772. }
  773. }
  774. }