Мой текущий лучший способ реализовать не надоедливый, мелкозернистый и масштабируемый контроль доступа в приложении Symfony с базой данных доктрины.

Почему это сложно?

Symfony предоставляет только один встроенный способ проверки доступа. Избиратели.

Они глубоко интегрированы в symfony и позволяют довольно легко проверять доступ. Он даже интегрирован в несколько собственных и сторонних пакетов.

  • В php с использованием $security->isGranted('edit' $object)
  • Аннотации с использованием @IsGranted(‘edit’, subject="object")
  • Шаблоны веточек {%if is_granted('edit', object) %}
  • Рабочие процессы с использованием guard: is_granted('edit', subject)
  • Security.yaml с использованием roles: edit, который неявно получает пользователя
  • ApiPlatform @ApiResource(security: 'is_granted("edit", object)')

Так что мы определенно захотим использовать их, чтобы воспользоваться преимуществами этой глубокой интеграции. Но как их определить?

Определение прав доступа

Официально вы создаете класс избирателя для каждого типа объекта, который хотите проверить. Это отлично работает, если у вас есть 1 или 2 объекта. Но если у вас когда-либо был проект с более чем 10 организациями / избирателями, вы заметите, что он становится громоздким.

Также часто бывает, что ProductVariant наследует большую часть / все права родительского Product.

  • Вы можете создать ProductVariantVoter, который просто вызывает isGranted на Product. Это увеличивает количество избирателей, вызываемых при каждой проверке доступа, а также является высоким по шкале «раздражающих усилий».
  • Вы можете сделать ProductVoter также проверку на ProductVariant, но удачи в выяснении, какие избиратели действуют на какие объекты, когда становится сложнее.

Но вся избирательная система имеет гораздо большую проблему, чем просто организация.

Листинг юридических лиц

Визуализируйте задачу: отобразите все Product ', которые видит пользователь. Или вообще отобразить любой список сущностей. Единственная возможность у избирателей - проверить каждый пункт.

Излишне говорить: это не сработает.

Обычно вы выполняете проверку на своем уровне sql. Что-то вроде product.public = 1 или product.owner = :user.

Но это не масштабируется на уровне архитектуры,

поскольку выполнение этого каждый раз, когда вы запрашиваете сущности, приведет к множеству немного разных реализаций, и это становится трудным в обслуживании, особенно если проверка более сложна, чем простое сравнение.

Мое лучшее решение на данный момент

Вам нужны 2 вещи.

  1. Центральный способ привязать права доступа к запросам доктрины.
  2. Центральный способ настройки избирателей

Так почему бы не определить их напрямую для объекта?

Кажется немного неправильным запускать проверки безопасности непосредственно в Entity, но поскольку он действительно содержит модель, а доступ является частью модели.

Это также означает, что обнаруживаемость очевидна. Вы хотите знать, кто может редактировать Product? Просто посмотрите на сущность. Это довольно очевидно.

Но чтобы это сработало, нам нужно будет определить Voter и использовать queryRestrictions всякий раз, когда мы запрашиваем продукты.

Выполнение

Пример использования

Теперь, когда ваш Избиратель реализован, вы можете использовать все разные способы, чтобы проверить, можете ли вы получить доступ к объекту:

public function show(Product $product)
{
    $this->denyAccessUnlessGranted('show', $product);
    
    // ... show implementation
}

И если вы хотите перечислить объект, у вас также есть четко определенный способ сделать это:

public function list(EntityManagerInterface $em, Security $security)
{
    $qb = $em->createQueryBuilder()
        ->select("product")
        ->from(Product::class, "product");
        
    Product::applyQueryRestrictions($security, $qb, "product");
    // ... list implementation
}

Никаких догадок и жесткого кодирования правил безопасности.

Использование ApiPlatform

Избиратели можно легко настроить в аннотации ApiResource.

В этом случае я буду использовать атрибуты GET, POST, PUT и т. Д. Однако вам, скорее всего, потребуется добавить некоторую специальную обработку для создания объектов, поскольку первая проверка безопасности просто пройдет null при первоначальном создании.

#[ApiResource(
security:
'request.isMethodSafe() or is_granted(request.getMethod(), object)'
securityPostDenormalize: 
'request.isMethodSafe() or is_granted(request.getMethod(), object)'
)]

Для запросов списка и получения вы можете использовать интерфейсы queryExtension:

class EntityRestrictionExtension
implements QueryCollectionExtensionInterface,
           QueryItemExtensionInterface
{
    private Security $security;
    public function applyToCollection($qb, $_, $class /* ... */)
    {
        $this->apply($qb, $class);
    }
    public function applyToItem($qb, $_, $class /* ... */)
    {
        $this->apply($qb, $class);
    }
    private function apply($qb, $class)
    {
        if (is_a($class, EntityRestrictionInterface::class, true)) {
            $class::applyQueryRestrictions($this->security, $qb, $qb->getRootAliases()[0]);
        }
    }
}

Некоторые лучшие практики

Сохраните 2 реализации доступа похожими

Постарайтесь сохранить атрибут show и applyQueryRestriction аналогичным.

Я всегда комментирую случаи ограничений, чтобы я мог легко увидеть, какие случаи я уже реализовал, даже если реализация db и voter сильно различаются.

public static function applyQueryRestrictions(Security $security, QueryBuilder $queryBuilder, string $alias)
{
    // check admin
    if ($security->isGranted('ROLE_ADMIN')) {
        return;
    }
        
    // check public
    $queryBuilder->andWhere("$alias.public = 1");
}
public function isGranted(Security $security, string $attribute)
{
    // 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;
}

Не абстрагируйся слишком много

Правила доступа абсолютно важны. Если вы напортачите со своей обычной логикой приложения, вы получите жалобу. Если вы испортите правила доступа, вы можете случайно раскрыть конфиденциальные данные. Этого никогда не должно случиться.

Убедитесь, что вы можете прочитать правило сверху вниз, чтобы вы могли легко понять, при каких обстоятельствах у кого есть доступ.

Всегда предоставляйте и никогда не ограничивайте доступ в зависимости от роли.

Допустим, ваша задача - скрыть некоторые объекты от клиентов в системе с клиентами, менеджерами и администраторами.

Очевидная реализация могла бы быть такой:

if ($security->isGranted('ROLE_CUSTOMER')) {
    return false; // Don't do this
}

На первый взгляд это правило выглядит хорошо. Но он неявно предоставляет доступ людям без роли. Вы всегда должны искать самую низкую роль, которая может что-то делать, например:

if ($security->isGranted('ROLE_MANAGER')) {
    return true; // Do this
}

Сохраняйте единообразие своих атрибутов

Я стараюсь придерживаться некоторых основ

  • show очевиден. Может ли кто-нибудь вообще увидеть эту сущность. Дальнейшие ограничения могут появиться позже.
  • edit тоже несколько очевидно. Может ли пользователь вообще редактировать эту сущность.
  • Избегайте create. Он почти всегда идентичен edit.

Но что, если вам нужен более детальный контроль? У вас есть 2 варианта:

  1. Для действий составьте больше глаголов, например impersonate, apply, sync.
  2. Если ваша сущность состоит из нескольких областей, вы можете попробовать такую ​​стратегию: {show,edit}_{share,description,owner}, но обычно лучше иметь несколько сущностей, как бы это ни раздражало.

Проверить дважды

Если вы изменяете объект, вы хотите проверить правильность редактирования до и после, когда вы изменили объект. Вы хотите увидеть, что a не может потерять доступ, например. смена собственника.

Если вы создаете объект, вы можете опустить первую проверку, поскольку проверки доступа, такие как «право собственности», не работают с пустыми объектами. Но никогда не нужно представлять пользователю форму, которую нельзя отправить.

/**
 * @Route("/product/new")
 */
public function new(Request $request)
{
    $product = new Product();
    // set everything to establish ownership
    return $this->edit($product, $request);
}
/**
 * @Route("/product/{id}")
 */
public function edit(Product $product, Request $request)
{
    // check 1: before edit and create
    $this->denyAccessUnlessGranted('edit', $product);
    $form = $this->createForm(ProductType::class, $product)
        ->add('submit', SubmitType::class);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        // check 2: after edit and create
        $this->denyAccessUnlessGranted('edit', $product);
        $this->persistAndFlush($product);
        return $this->redirectToRoute('app_product_edit', [
            'id' => $application->getId(),
        ]);
    }
    return $this->render('ProductController/edit.html.twig', [
        'form' => $form->createView(),
    ]);
}

Понятия не имею, как правильно проверять наличие

В многостраничном приложении я действительно хотел бы проверить, нужно ли показывать ссылку на создание. Поскольку объекта нет, это невозможно сделать с помощью этих шаблонов.

Если у вас есть родительский объект, вы можете проверить это с помощью общего редактирования или специального атрибута create. В противном случае вам придется просто проверять наличие ролей напрямую.

Отказ от ответственности

Вам все еще нужно знать, что вы делаете. Я просто предлагаю, как бы вы могли справиться со своими привилегиями.