Skip to content

Instantly share code, notes, and snippets.

@Nemo64
Last active September 30, 2022 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nemo64/7a8fbcc6727c502b8cd8cbd7ef36d454 to your computer and use it in GitHub Desktop.
Save Nemo64/7a8fbcc6727c502b8cd8cbd7ef36d454 to your computer and use it in GitHub Desktop.
centralized symfony access control
<?php
namespace App\Security;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
/**
* Allows access control to be defined within an entity.
*/
interface EntityRestrictionInterface
{
/**
* Adds _read_ checks to a query builder.
*
* @param Security $security
* @param QueryBuilder $queryBuilder
* @param string $alias The root alias that represents $this.
* @throws AccessDeniedException if you shouldn't be able to query this entity in the first place
*/
public static function applyQueryRestrictions(Security $security, QueryBuilder $queryBuilder, string $alias): void;
/**
* Executes _write_ and _read_ checks.
*
* This is usually very similar to {@see applyQueryRestrictions}, just implemented in php.
*
* @param Security $security
* @param string $attribute Can be anything that is thrown into {@see Security::isGranted()}
* @return bool {@see \Symfony\Component\Security\Core\Authorization\Voter\Voter::voteOnAttribute}
*/
public function isGranted(Security $security, string $attribute): bool;
}
<?php
namespace App\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
/**
* This is the voter for {@see EntityRestrictionInterface} that uses the
* {@see EntityRestrictionInterface::isGranted} method to vote.
*/
class EntityRestrictionVoter extends Voter
{
private Security $security;
public function __construct(Security $security) {
$this->security = $security;
}
protected function supports($attribute, $subject)
{
return $subject instanceof EntityRestrictionInterface;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** @var EntityRestrictionInterface $subject */
return $subject->isGranted($this->security, $attribute);
}
}
<?php
namespace App\Entity;
use App\Security\EntityRestrictionInterface;
use App\Security\Security;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\QueryBuilder;
/** @ORM\Entity() */
class Product implements EntityRestrictionInterface
{
/** @ORM\Column(type="string") */
public string $name;
/** @ORM\Column(type="boolean", options={"default": 0}) */
public bool $public = false;
public static function applyQueryRestrictions(Security $security, QueryBuilder $queryBuilder, string $alias): void
{
// check admin
if ($security->isGranted('ROLE_ADMIN')) {
return;
}
// check public
$queryBuilder->andWhere("$alias.public = 1");
}
/** @noinspection InArrayMissUseInspection, NotOptimalIfConditionsInspection */
public function isGranted(Security $security, string $attribute): bool
{
// check admin
if (in_array($attribute, ['show', 'edit']) && $security->isGranted('ROLE_ADMIN')) {
return true;
}
// check public
if (in_array($attribute, ['show']) && $this->public) {
return true;
}
return false;
}
}
<?php
namespace App\Controller;
use App\Entity\Product;
use App\Security\Security;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class ProductController extends AbstractController
{
public function list(EntityManagerInterface $em, Security $security)
{
$qb = $em->createQueryBuilder()
->select("product")
->from(Product::class, "product");
Product::applyQueryRestrictions($security, $qb, "product");
return $this->json($qb->getQuery()->getResult());
}
public function show(Product $product)
{
$this->denyAccessUnlessGranted('show', $product);
return $this->json($product);
}
}
<?php
namespace App\Entity;
use App\Security\EntityRestrictionInterface;
use App\Security\Security;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\QueryBuilder;
/** @ORM\Entity() */
class ProductVariant implements EntityRestrictionInterface
{
/** @ORM\ManyToOne(targetEntity=Product::class) */
public Product $product;
/** @ORM\Column(type="string") */
public string $sku;
public static function applyQueryRestrictions(Security $security, QueryBuilder $queryBuilder, string $alias): void
{
// delegate access check
$queryBuilder->join("$alias.product", "{$alias}_product");
Product::applyQueryRestrictions($security, $queryBuilder, "{$alias}_product");
}
public function isGranted(Security $security, string $attribute): bool
{
// delegate access check
return $security->isGranted($attribute, $this->product);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment