Skip to content

Commit

Permalink
feat(Attributes): Secure API collection and item attribute_values req…
Browse files Browse the repository at this point in the history
…uests with realms
  • Loading branch information
ambroisemaupate committed Sep 5, 2023
1 parent 6f5f477 commit fd68f51
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace RZ\Roadiz\CoreBundle\Api\Extension;

use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
use RZ\Roadiz\CoreBundle\Entity\AttributeValue;
use RZ\Roadiz\CoreBundle\Model\RealmInterface;
use RZ\Roadiz\CoreBundle\Realm\RealmResolverInterface;
use Symfony\Component\Security\Core\Security;

final class AttributeValueRealmExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(
private Security $security,
private RealmResolverInterface $realmResolver
) {
}

public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
$this->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())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 4 additions & 3 deletions lib/RoadizCoreBundle/src/Entity/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions lib/RoadizCoreBundle/src/Entity/AttributeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions lib/RoadizCoreBundle/src/Model/AttributeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,7 @@ public function isEnum(): bool;
* @return bool
*/
public function isCountry(): bool;

public function getDefaultRealm(): ?RealmInterface;
public function setDefaultRealm(?RealmInterface $defaultRealm): self;
}
5 changes: 4 additions & 1 deletion lib/RoadizCoreBundle/src/Model/AttributeValueInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion lib/RoadizCoreBundle/src/Model/RealmInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
29 changes: 28 additions & 1 deletion lib/RoadizCoreBundle/src/Realm/RealmResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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();
}
}
10 changes: 10 additions & 0 deletions lib/RoadizCoreBundle/src/Realm/RealmResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit fd68f51

Please sign in to comment.