vendor/symfony/security-acl/Dbal/MutableAclProvider.php line 43

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