MutableAclProvider.php 36 KB

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