diff --git a/lib/RoadizCoreBundle/src/Api/Extension/AttributeValueRealmExtension.php b/lib/RoadizCoreBundle/src/Api/Extension/AttributeValueRealmExtension.php new file mode 100644 index 00000000..a43c17fd --- /dev/null +++ b/lib/RoadizCoreBundle/src/Api/Extension/AttributeValueRealmExtension.php @@ -0,0 +1,69 @@ +addWhere($queryBuilder, $resourceClass); + } + + public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void + { + $this->addWhere($queryBuilder, $resourceClass); + } + + private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void + { + if ($resourceClass !== AttributeValue::class || $this->security->isGranted('ROLE_ACCESS_NODE_ATTRIBUTES')) { + return; + } + + /* + * Filter out all attribute values requiring a realm for anonymous users. + */ + $rootAlias = $queryBuilder->getRootAliases()[0]; + if ($this->security->isGranted('IS_ANONYMOUS')) { + $queryBuilder->andWhere($queryBuilder->expr()->isNull(sprintf('%s.realm', $rootAlias))); + return; + } + + /* + * Filter all attribute values requiring a granted realm or no realm for current user. + */ + $queryBuilder->andWhere($queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull(sprintf('%s.realm', $rootAlias)), + $queryBuilder->expr()->in( + sprintf('%s.realm', $rootAlias), + ':realmIds' + ) + ))->setParameter('realmIds', $this->getGrantedRealmIds()); + } + + private function getGrantedRealmIds(): array + { + return array_map( + fn (RealmInterface $realm) => $realm->getId(), + array_filter($this->realmResolver->getGrantedRealms()) + ); + } +} diff --git a/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php b/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php index 23ab758a..4e7ada54 100644 --- a/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php +++ b/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php @@ -31,7 +31,7 @@ public function getSubscribedEvents(): array public function prePersist(LifecycleEventArgs $event): void { $entity = $event->getObject(); - if ($entity instanceof AttributeValue) { + if ($entity instanceof AttributeValueInterface) { if ( null !== $entity->getAttribute() && null !== $entity->getAttribute()->getDefaultRealm() diff --git a/lib/RoadizCoreBundle/src/Entity/Attribute.php b/lib/RoadizCoreBundle/src/Entity/Attribute.php index 259d8026..1b97e397 100644 --- a/lib/RoadizCoreBundle/src/Entity/Attribute.php +++ b/lib/RoadizCoreBundle/src/Entity/Attribute.php @@ -8,6 +8,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; +use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Repository\AttributeRepository; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation as SymfonySerializer; @@ -59,7 +60,7 @@ class Attribute extends AbstractEntity implements AttributeInterface )] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - private ?Realm $defaultRealm = null; + private ?RealmInterface $defaultRealm = null; public function __construct() { @@ -88,12 +89,12 @@ public function setAttributeDocuments(Collection $attributeDocuments): Attribute return $this; } - public function getDefaultRealm(): ?Realm + public function getDefaultRealm(): ?RealmInterface { return $this->defaultRealm; } - public function setDefaultRealm(?Realm $defaultRealm): Attribute + public function setDefaultRealm(?RealmInterface $defaultRealm): Attribute { $this->defaultRealm = $defaultRealm; return $this; diff --git a/lib/RoadizCoreBundle/src/Entity/AttributeValue.php b/lib/RoadizCoreBundle/src/Entity/AttributeValue.php index 6b77ca8d..641fd2fa 100644 --- a/lib/RoadizCoreBundle/src/Entity/AttributeValue.php +++ b/lib/RoadizCoreBundle/src/Entity/AttributeValue.php @@ -5,8 +5,8 @@ namespace RZ\Roadiz\CoreBundle\Entity; use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; @@ -15,6 +15,7 @@ use RZ\Roadiz\CoreBundle\Model\AttributeValueInterface; use RZ\Roadiz\CoreBundle\Model\AttributeValueTrait; use RZ\Roadiz\CoreBundle\Model\AttributeValueTranslationInterface; +use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Repository\AttributeValueRepository; use Symfony\Component\Serializer\Annotation as SymfonySerializer; @@ -66,7 +67,7 @@ class AttributeValue extends AbstractPositioned implements AttributeValueInterfa )] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - private ?Realm $realm = null; + private ?RealmInterface $realm = null; public function __construct() { @@ -124,12 +125,12 @@ public function setNode(?Node $node): AttributeValue return $this; } - public function getRealm(): ?Realm + public function getRealm(): ?RealmInterface { return $this->realm; } - public function setRealm(?Realm $realm): AttributeValue + public function setRealm(?RealmInterface $realm): AttributeValue { $this->realm = $realm; return $this; diff --git a/lib/RoadizCoreBundle/src/Model/AttributeInterface.php b/lib/RoadizCoreBundle/src/Model/AttributeInterface.php index f22e2ec2..615500c4 100644 --- a/lib/RoadizCoreBundle/src/Model/AttributeInterface.php +++ b/lib/RoadizCoreBundle/src/Model/AttributeInterface.php @@ -215,4 +215,7 @@ public function isEnum(): bool; * @return bool */ public function isCountry(): bool; + + public function getDefaultRealm(): ?RealmInterface; + public function setDefaultRealm(?RealmInterface $defaultRealm): self; } diff --git a/lib/RoadizCoreBundle/src/Model/AttributeValueInterface.php b/lib/RoadizCoreBundle/src/Model/AttributeValueInterface.php index 8bb43e55..aab86a8c 100644 --- a/lib/RoadizCoreBundle/src/Model/AttributeValueInterface.php +++ b/lib/RoadizCoreBundle/src/Model/AttributeValueInterface.php @@ -11,8 +11,11 @@ interface AttributeValueInterface extends PositionedInterface, PersistableInterface { + public function getRealm(): ?RealmInterface; + public function setRealm(?RealmInterface $realm): self; + /** - * @return AttributeInterface + * @return AttributeInterface|null */ public function getAttribute(): ?AttributeInterface; diff --git a/lib/RoadizCoreBundle/src/Model/RealmInterface.php b/lib/RoadizCoreBundle/src/Model/RealmInterface.php index e637d108..6518eca8 100644 --- a/lib/RoadizCoreBundle/src/Model/RealmInterface.php +++ b/lib/RoadizCoreBundle/src/Model/RealmInterface.php @@ -5,8 +5,9 @@ namespace RZ\Roadiz\CoreBundle\Model; use Doctrine\Common\Collections\Collection; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -interface RealmInterface +interface RealmInterface extends PersistableInterface { public const TYPE_PLAIN_PASSWORD = 'plain_password'; public const TYPE_ROLE = 'bearer_role'; diff --git a/lib/RoadizCoreBundle/src/Realm/RealmResolver.php b/lib/RoadizCoreBundle/src/Realm/RealmResolver.php index fa4f0bf2..f987c954 100644 --- a/lib/RoadizCoreBundle/src/Realm/RealmResolver.php +++ b/lib/RoadizCoreBundle/src/Realm/RealmResolver.php @@ -5,6 +5,7 @@ namespace RZ\Roadiz\CoreBundle\Realm; use Doctrine\Persistence\ManagerRegistry; +use Psr\Cache\CacheItemPoolInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Realm; use RZ\Roadiz\CoreBundle\Model\RealmInterface; @@ -16,11 +17,13 @@ final class RealmResolver implements RealmResolverInterface { private ManagerRegistry $managerRegistry; private Security $security; + private CacheItemPoolInterface $cache; - public function __construct(ManagerRegistry $managerRegistry, Security $security) + public function __construct(ManagerRegistry $managerRegistry, Security $security, CacheItemPoolInterface $cache) { $this->managerRegistry = $managerRegistry; $this->security = $security; + $this->cache = $cache; } public function getRealms(?Node $node): array @@ -45,4 +48,28 @@ public function denyUnlessGranted(RealmInterface $realm): void ); } } + + public function getGrantedRealms(): array + { + $cacheItem = $this->cache->getItem('granted_realms'); + if (!$cacheItem->isHit()) { + $allRealms = $this->managerRegistry->getRepository(Realm::class)->findBy([]); + $cacheItem->set(array_filter($allRealms, fn(RealmInterface $realm) => $this->isGranted($realm))); + $cacheItem->expiresAfter(new \DateInterval('PT1H')); + $this->cache->save($cacheItem); + } + return $cacheItem->get(); + } + + public function getDeniedRealms(): array + { + $cacheItem = $this->cache->getItem('denied_realms'); + if (!$cacheItem->isHit()) { + $allRealms = $this->managerRegistry->getRepository(Realm::class)->findBy([]); + $cacheItem->set(array_filter($allRealms, fn(RealmInterface $realm) => !$this->isGranted($realm))); + $cacheItem->expiresAfter(new \DateInterval('PT1H')); + $this->cache->save($cacheItem); + } + return $cacheItem->get(); + } } diff --git a/lib/RoadizCoreBundle/src/Realm/RealmResolverInterface.php b/lib/RoadizCoreBundle/src/Realm/RealmResolverInterface.php index 970b094f..fd0ca8cb 100644 --- a/lib/RoadizCoreBundle/src/Realm/RealmResolverInterface.php +++ b/lib/RoadizCoreBundle/src/Realm/RealmResolverInterface.php @@ -23,4 +23,14 @@ public function isGranted(RealmInterface $realm): bool; * @throws UnauthorizedHttpException */ public function denyUnlessGranted(RealmInterface $realm): void; + + /** + * @return RealmInterface[] Return all realms granted to current user. + */ + public function getGrantedRealms(): array; + + /** + * @return RealmInterface[] Return all realms denied from current user. + */ + public function getDeniedRealms(): array; }