MidТеория15 min

Модели контроля доступа

ACL, RBAC, ABAC, ReBAC — модели контроля доступа и реализация RBAC через Symfony Voters

Обзор моделей

Модель Расшифровка Суть Сложность
ACL Access Control List Список прав для каждого ресурса Низкая
RBAC Role-Based Access Control Права через роли Средняя
ABAC Attribute-Based Access Control Права через атрибуты Высокая
ReBAC Relationship-Based Access Control Права через связи Высокая

ACL (Access Control List)

ACL — простейшая модель: для каждого ресурса определён список субъектов и их прав.

Файл: report.pdf
├── user:alice → read, write
├── user:bob → read
└── group:managers → read, write, delete
Плюсы Минусы
Простая реализация Не масштабируется
Понятная логика Сложно управлять при 1000+ ресурсах
Гранулярный контроль Нет наследования

RBAC (Role-Based Access Control)

RBAC — права назначаются ролям, пользователи получают роли. Самая распространённая модель для бизнес-приложений.

Иерархия RBAC

Permission: "order.create", "order.view", "order.delete"
     ↑
   Role: "manager" → ["order.create", "order.view", "order.delete"]
   Role: "viewer"  → ["order.view"]
     ↑
   User: alice → ["manager"]
   User: bob   → ["viewer"]

RBAC реализация

<?php

declare(strict_types=1);

namespace App\Security;

enum Permission: string
{
    case OrderView = 'order.view';
    case OrderCreate = 'order.create';
    case OrderEdit = 'order.edit';
    case OrderDelete = 'order.delete';
    case UserManage = 'user.manage';
    case ReportView = 'report.view';
    case ReportExport = 'report.export';
    case SettingsManage = 'settings.manage';
}

enum Role: string
{
    case Admin = 'ROLE_ADMIN';
    case Manager = 'ROLE_MANAGER';
    case Operator = 'ROLE_OPERATOR';
    case Viewer = 'ROLE_VIEWER';

    /**
     * Get permissions for this role.
     *
     * @return array<Permission>
     */
    public function permissions(): array
    {
        return match ($this) {
            self::Admin => Permission::cases(), // All permissions
            self::Manager => [
                Permission::OrderView, Permission::OrderCreate,
                Permission::OrderEdit, Permission::OrderDelete,
                Permission::ReportView, Permission::ReportExport,
            ],
            self::Operator => [
                Permission::OrderView, Permission::OrderCreate,
                Permission::OrderEdit,
            ],
            self::Viewer => [
                Permission::OrderView, Permission::ReportView,
            ],
        };
    }

    /**
     * Check role hierarchy — higher roles inherit lower role permissions.
     *
     * @return array<Role>
     */
    public function inherits(): array
    {
        return match ($this) {
            self::Admin => [self::Manager, self::Operator, self::Viewer],
            self::Manager => [self::Operator, self::Viewer],
            self::Operator => [self::Viewer],
            self::Viewer => [],
        };
    }
}

final readonly class RbacChecker
{
    /**
     * Check if user with given roles has a specific permission.
     *
     * @param array<Role> $userRoles
     */
    public function hasPermission(array $userRoles, Permission $permission): bool
    {
        foreach ($userRoles as $role) {
            if (in_array($permission, $role->permissions(), true)) {
                return true;
            }

            // Check inherited roles
            foreach ($role->inherits() as $inheritedRole) {
                if (in_array($permission, $inheritedRole->permissions(), true)) {
                    return true;
                }
            }
        }

        return false;
    }
}
### Symfony Voters

Symfony Voters — механизм для реализации сложной логики авторизации.

<?php

declare(strict_types=1);

namespace App\Security\Voter;

use App\Entity\Order;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

/**
 * Voter for Order entity access control.
 *
 * @extends Voter<string, Order>
 */
final class OrderVoter extends Voter
{
    public const VIEW = 'ORDER_VIEW';
    public const EDIT = 'ORDER_EDIT';
    public const DELETE = 'ORDER_DELETE';
    public const CANCEL = 'ORDER_CANCEL';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE, self::CANCEL], true)
            && $subject instanceof Order;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        /** @var Order $order */
        $order = $subject;

        return match ($attribute) {
            self::VIEW => $this->canView($order, $user),
            self::EDIT => $this->canEdit($order, $user),
            self::DELETE => $this->canDelete($order, $user),
            self::CANCEL => $this->canCancel($order, $user),
            default => false,
        };
    }

    private function canView(Order $order, User $user): bool
    {
        // Admin can view all orders
        if ($user->hasRole('ROLE_ADMIN')) {
            return true;
        }

        // Manager can view orders from their department
        if ($user->hasRole('ROLE_MANAGER')
            && $order->getDepartment() === $user->getDepartment()
        ) {
            return true;
        }

        // Owner can view their own orders
        return $order->getCreatedBy() === $user->getId();
    }

    private function canEdit(Order $order, User $user): bool
    {
        // Cannot edit completed or cancelled orders
        if ($order->isCompleted() || $order->isCancelled()) {
            return false;
        }

        if ($user->hasRole('ROLE_ADMIN')) {
            return true;
        }

        // Only owner can edit, and only within 24 hours
        return $order->getCreatedBy() === $user->getId()
            && $order->getCreatedAt() > new \DateTimeImmutable('-24 hours');
    }

    private function canDelete(Order $order, User $user): bool
    {
        // Only admins can delete
        return $user->hasRole('ROLE_ADMIN');
    }

    private function canCancel(Order $order, User $user): bool
    {
        if ($order->isCancelled() || $order->isCompleted()) {
            return false;
        }

        return $user->hasRole('ROLE_ADMIN')
            || $order->getCreatedBy() === $user->getId();
    }
}
## ABAC (Attribute-Based Access Control)

ABAC принимает решения на основе атрибутов субъекта, ресурса, действия и окружения.

Subject attributes:  role=manager, department=sales, clearance=high
Resource attributes: type=order, status=draft, classification=internal
Action attributes:   type=edit
Environment:         time=09:00-17:00, ip=office_range, day=weekday

Policy: ALLOW if
  subject.role = "manager" AND
  resource.department = subject.department AND
  environment.time IN working_hours AND
  resource.classification <= subject.clearance
<?php

declare(strict_types=1);

namespace App\Security\Abac;

enum PolicyEffect: string
{
    case Allow = 'allow';
    case Deny = 'deny';
}

final readonly class AbacPolicy
{
    public function __construct(
        public string $name,
        // A typed effect, not a string: a policy written with 'Allow' or
        // 'permit' would silently behave as deny and never be noticed.
        public PolicyEffect $effect,
        /** @var array<string, mixed> */
        public array $subjectConditions,
        /** @var array<string, mixed> */
        public array $resourceConditions,
        /** @var array<string, mixed> */
        public array $environmentConditions,
    ) {}
}

final class AbacEngine
{
    /** @var array<AbacPolicy> */
    private array $policies = [];

    public function addPolicy(AbacPolicy $policy): void
    {
        $this->policies[] = $policy;
    }

    /**
     * Evaluate access request against all policies.
     *
     * @param array<string, mixed> $subject User attributes
     * @param array<string, mixed> $resource Resource attributes
     * @param string $action Requested action
     * @param array<string, mixed> $environment Environment context
     */
    public function isAllowed(
        array $subject,
        array $resource,
        string $action,
        array $environment = [],
    ): bool {
        foreach ($this->policies as $policy) {
            if ($this->matchesConditions($subject, $policy->subjectConditions)
                && $this->matchesConditions($resource, $policy->resourceConditions)
                && $this->matchesConditions($environment, $policy->environmentConditions)
            ) {
                return $policy->effect === PolicyEffect::Allow;
            }
        }

        // Default deny — an unmatched request is never granted.
        return false;
    }

    /**
     * @param array<string, mixed> $attributes
     * @param array<string, mixed> $conditions
     */
    private function matchesConditions(array $attributes, array $conditions): bool
    {
        foreach ($conditions as $key => $expected) {
            // array_key_exists, not isset: an attribute explicitly set to null
            // is present and must be compared, not treated as missing.
            if (!array_key_exists($key, $attributes)) {
                return false;
            }

            if (is_array($expected)) {
                if (!in_array($attributes[$key], $expected, true)) {
                    return false;
                }
            } elseif ($attributes[$key] !== $expected) {
                return false;
            }
        }

        return true;
    }
}
## ReBAC (Relationship-Based Access Control)

ReBAC определяет доступ через отношения (связи) между субъектами и объектами. Пример: Google Docs — «Вы можете редактировать, потому что владелец поделился с вашей командой».

Relationships:
  document:report#owner@user:alice
  document:report#editor@team:engineering
  team:engineering#member@user:bob

Check: can user:bob edit document:report?
→ bob is member of team:engineering
→ team:engineering is editor of document:report
→ YES
Система Описание
Google Zanzibar Система авторизации Google
SpiceDB Open-source ReBAC (Zanzibar-inspired)
OpenFGA Auth0 / Okta ReBAC engine

Сравнение моделей

Критерий ACL RBAC ABAC ReBAC
Сложность Низкая Средняя Высокая Высокая
Гибкость Низкая Средняя Очень высокая Высокая
Масштабируемость Плохая Хорошая Хорошая Отличная
Аудит Простой Простой Сложный Средний
Когда < 100 ресурсов Бизнес-роли Сложные политики Социальные графы

Рекомендация: Начинайте с RBAC. Если его не хватает — добавляйте элементы ABAC (Symfony Voters). ReBAC нужен для систем с sharing (документы, проекты, команды).

Итоги

Модель Когда использовать
ACL Простые системы, файловые разрешения
RBAC Бизнес-приложения с чёткими ролями
ABAC Сложные политики с контекстом
ReBAC Системы с sharing и организационной структурой
Symfony Voters Гибкая авторизация в Symfony-приложениях