<?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;
}
}
package security
// Permission represents an application permission.
type Permission string
const (
PermOrderView Permission = "order.view"
PermOrderCreate Permission = "order.create"
PermOrderEdit Permission = "order.edit"
PermOrderDelete Permission = "order.delete"
PermUserManage Permission = "user.manage"
PermReportView Permission = "report.view"
PermReportExport Permission = "report.export"
PermSettingsManage Permission = "settings.manage"
)
// Role represents a user role with permissions.
type Role string
const (
RoleAdmin Role = "ROLE_ADMIN"
RoleManager Role = "ROLE_MANAGER"
RoleOperator Role = "ROLE_OPERATOR"
RoleViewer Role = "ROLE_VIEWER"
)
// Permissions returns the permissions granted directly by a role.
func (r Role) Permissions() []Permission {
switch r {
case RoleAdmin:
return []Permission{
PermOrderView, PermOrderCreate, PermOrderEdit, PermOrderDelete,
PermUserManage, PermReportView, PermReportExport, PermSettingsManage,
}
case RoleManager:
return []Permission{
PermOrderView, PermOrderCreate, PermOrderEdit, PermOrderDelete,
PermReportView, PermReportExport,
}
case RoleOperator:
return []Permission{PermOrderView, PermOrderCreate, PermOrderEdit}
case RoleViewer:
return []Permission{PermOrderView, PermReportView}
default:
// Role is a string type, so an unknown value is possible. Granting
// nothing keeps the switch fail-closed instead of falling through.
return nil
}
}
// Inherits returns the lower roles whose permissions this role also gets.
func (r Role) Inherits() []Role {
switch r {
case RoleAdmin:
return []Role{RoleManager, RoleOperator, RoleViewer}
case RoleManager:
return []Role{RoleOperator, RoleViewer}
case RoleOperator:
return []Role{RoleViewer}
case RoleViewer:
return nil
default:
return nil
}
}
func (r Role) grants(perm Permission) bool {
for _, p := range r.Permissions() {
if p == perm {
return true
}
}
return false
}
// HasPermission checks if a set of roles includes a specific permission,
// directly or through the role hierarchy.
func HasPermission(roles []Role, perm Permission) bool {
for _, role := range roles {
if role.grants(perm) {
return true
}
for _, inherited := range role.Inherits() {
if inherited.grants(perm) {
return true
}
}
}
return false
}
namespace App.Security;
public enum Permission
{
OrderView,
OrderCreate,
OrderEdit,
OrderDelete,
UserManage,
ReportView,
ReportExport,
SettingsManage,
}
public enum Role
{
Admin,
Manager,
Operator,
Viewer,
}
public static class RoleDefinitions
{
// Permissions granted directly by a role.
public static IReadOnlySet<Permission> Permissions(this Role role) => role switch
{
Role.Admin => Enum.GetValues<Permission>().ToHashSet(),
Role.Manager =>
[
Permission.OrderView, Permission.OrderCreate,
Permission.OrderEdit, Permission.OrderDelete,
Permission.ReportView, Permission.ReportExport,
],
Role.Operator =>
[
Permission.OrderView, Permission.OrderCreate, Permission.OrderEdit,
],
Role.Viewer => [Permission.OrderView, Permission.ReportView],
_ => throw new ArgumentOutOfRangeException(nameof(role), role, "Unknown role"),
};
// Role hierarchy — higher roles inherit lower role permissions.
public static IReadOnlyList<Role> Inherits(this Role role) => role switch
{
Role.Admin => [Role.Manager, Role.Operator, Role.Viewer],
Role.Manager => [Role.Operator, Role.Viewer],
Role.Operator => [Role.Viewer],
Role.Viewer => [],
_ => throw new ArgumentOutOfRangeException(nameof(role), role, "Unknown role"),
};
}
public static class RbacChecker
{
// Check if a user holding the given roles has a specific permission.
public static bool HasPermission(IEnumerable<Role> userRoles, Permission permission)
=> userRoles.Any(role =>
role.Permissions().Contains(permission)
|| role.Inherits().Any(inherited => inherited.Permissions().Contains(permission)));
}
from collections.abc import Iterable
from enum import StrEnum
class Permission(StrEnum):
ORDER_VIEW = "order.view"
ORDER_CREATE = "order.create"
ORDER_EDIT = "order.edit"
ORDER_DELETE = "order.delete"
USER_MANAGE = "user.manage"
REPORT_VIEW = "report.view"
REPORT_EXPORT = "report.export"
SETTINGS_MANAGE = "settings.manage"
class Role(StrEnum):
ADMIN = "ROLE_ADMIN"
MANAGER = "ROLE_MANAGER"
OPERATOR = "ROLE_OPERATOR"
VIEWER = "ROLE_VIEWER"
# Permissions granted directly by a role.
ROLE_PERMISSIONS: dict[Role, frozenset[Permission]] = {
Role.ADMIN: frozenset(Permission),
Role.MANAGER: frozenset(
{
Permission.ORDER_VIEW,
Permission.ORDER_CREATE,
Permission.ORDER_EDIT,
Permission.ORDER_DELETE,
Permission.REPORT_VIEW,
Permission.REPORT_EXPORT,
}
),
Role.OPERATOR: frozenset(
{Permission.ORDER_VIEW, Permission.ORDER_CREATE, Permission.ORDER_EDIT}
),
Role.VIEWER: frozenset({Permission.ORDER_VIEW, Permission.REPORT_VIEW}),
}
# Role hierarchy — higher roles inherit lower role permissions.
ROLE_INHERITS: dict[Role, tuple[Role, ...]] = {
Role.ADMIN: (Role.MANAGER, Role.OPERATOR, Role.VIEWER),
Role.MANAGER: (Role.OPERATOR, Role.VIEWER),
Role.OPERATOR: (Role.VIEWER,),
Role.VIEWER: (),
}
def effective_permissions(role: Role) -> frozenset[Permission]:
"""Direct permissions plus everything inherited from lower roles."""
granted = set(ROLE_PERMISSIONS[role])
for inherited in ROLE_INHERITS[role]:
granted |= ROLE_PERMISSIONS[inherited]
return frozenset(granted)
def has_permission(user_roles: Iterable[Role], permission: Permission) -> bool:
"""Check if a user holding the given roles has a specific permission."""
return any(permission in effective_permissions(role) for role in user_roles)
### 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();
}
}
package security
import "time"
// OrderAction is the operation being authorized on an Order.
type OrderAction string
const (
ActionOrderView OrderAction = "ORDER_VIEW"
ActionOrderEdit OrderAction = "ORDER_EDIT"
ActionOrderDelete OrderAction = "ORDER_DELETE"
ActionOrderCancel OrderAction = "ORDER_CANCEL"
)
// OrderVoter implements access control for Order entities.
type OrderVoter struct{}
// IsGranted dispatches an action to the matching rule.
func (v *OrderVoter) IsGranted(action OrderAction, order *Order, user *User) bool {
switch action {
case ActionOrderView:
return v.CanView(order, user)
case ActionOrderEdit:
return v.CanEdit(order, user)
case ActionOrderDelete:
return v.CanDelete(order, user)
case ActionOrderCancel:
return v.CanCancel(order, user)
default:
// Unknown operations are denied rather than silently allowed:
// a typo in a new action must not open the resource.
return false
}
}
// CanView checks if user can view the order.
func (v *OrderVoter) CanView(order *Order, user *User) bool {
if user.HasRole(RoleAdmin) {
return true
}
if user.HasRole(RoleManager) && order.Department == user.Department {
return true
}
return order.CreatedBy == user.ID
}
// CanEdit checks if user can edit the order.
func (v *OrderVoter) CanEdit(order *Order, user *User) bool {
if order.IsCompleted() || order.IsCancelled() {
return false
}
if user.HasRole(RoleAdmin) {
return true
}
return order.CreatedBy == user.ID &&
order.CreatedAt.After(time.Now().Add(-24*time.Hour))
}
// CanDelete checks if user can delete the order.
func (v *OrderVoter) CanDelete(order *Order, user *User) bool {
return user.HasRole(RoleAdmin)
}
// CanCancel checks if user can cancel the order.
func (v *OrderVoter) CanCancel(order *Order, user *User) bool {
if order.IsCancelled() || order.IsCompleted() {
return false
}
return user.HasRole(RoleAdmin) || order.CreatedBy == user.ID
}
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
namespace App.Security;
// ASP.NET Core's equivalent of a Symfony Voter is a resource-based
// AuthorizationHandler: one handler per requirement and resource type.
public sealed class OrderOperations
{
public static readonly OperationAuthorizationRequirement View = new() { Name = nameof(View) };
public static readonly OperationAuthorizationRequirement Edit = new() { Name = nameof(Edit) };
public static readonly OperationAuthorizationRequirement Delete = new() { Name = nameof(Delete) };
public static readonly OperationAuthorizationRequirement Cancel = new() { Name = nameof(Cancel) };
}
public sealed class OrderAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Order>
{
private static readonly TimeSpan EditWindow = TimeSpan.FromHours(24);
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Order order)
{
ClaimsPrincipal user = context.User;
bool granted = requirement.Name switch
{
nameof(OrderOperations.View) => CanView(order, user),
nameof(OrderOperations.Edit) => CanEdit(order, user),
nameof(OrderOperations.Delete) => user.IsInRole("Admin"),
nameof(OrderOperations.Cancel) => CanCancel(order, user),
// Unknown operations are denied rather than silently allowed.
_ => false,
};
if (granted)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
private static bool CanView(Order order, ClaimsPrincipal user)
{
// Admin can view all orders.
if (user.IsInRole("Admin"))
{
return true;
}
// Manager can view orders from their own department.
if (user.IsInRole("Manager")
&& order.Department == user.FindFirst("department")?.Value)
{
return true;
}
// Owner can view their own orders.
return order.CreatedBy == user.FindFirst("sub")?.Value;
}
private static bool CanEdit(Order order, ClaimsPrincipal user)
{
// Completed or cancelled orders are immutable.
if (order.IsCompleted || order.IsCancelled)
{
return false;
}
if (user.IsInRole("Admin"))
{
return true;
}
// Only the owner may edit, and only within 24 hours.
return order.CreatedBy == user.FindFirst("sub")?.Value
&& order.CreatedAt > DateTimeOffset.UtcNow - EditWindow;
}
private static bool CanCancel(Order order, ClaimsPrincipal user)
{
if (order.IsCompleted || order.IsCancelled)
{
return false;
}
return user.IsInRole("Admin") || order.CreatedBy == user.FindFirst("sub")?.Value;
}
}
// Usage in a controller:
// var result = await authorizationService.AuthorizeAsync(User, order, OrderOperations.Edit);
// if (!result.Succeeded) return Forbid();
from datetime import datetime, timedelta, timezone
from enum import StrEnum
EDIT_WINDOW = timedelta(hours=24)
class OrderAction(StrEnum):
VIEW = "ORDER_VIEW"
EDIT = "ORDER_EDIT"
DELETE = "ORDER_DELETE"
CANCEL = "ORDER_CANCEL"
class OrderPolicy:
"""Per-entity authorization rules, the equivalent of a Symfony Voter."""
def is_granted(self, action: OrderAction, order: Order, user: User) -> bool:
match action:
case OrderAction.VIEW:
return self._can_view(order, user)
case OrderAction.EDIT:
return self._can_edit(order, user)
case OrderAction.DELETE:
# Only admins can delete.
return "ROLE_ADMIN" in user.roles
case OrderAction.CANCEL:
return self._can_cancel(order, user)
def _can_view(self, order: Order, user: User) -> bool:
# Admin can view all orders.
if "ROLE_ADMIN" in user.roles:
return True
# Manager can view orders from their own department.
if "ROLE_MANAGER" in user.roles and order.department == user.department:
return True
# Owner can view their own orders.
return order.created_by == user.id
def _can_edit(self, order: Order, user: User) -> bool:
# Completed or cancelled orders are immutable.
if order.is_completed or order.is_cancelled:
return False
if "ROLE_ADMIN" in user.roles:
return True
# Only the owner may edit, and only within 24 hours.
return (
order.created_by == user.id
and order.created_at > datetime.now(timezone.utc) - EDIT_WINDOW
)
def _can_cancel(self, order: Order, user: User) -> bool:
if order.is_completed or order.is_cancelled:
return False
return "ROLE_ADMIN" in user.roles or order.created_by == user.id
## 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;
}
}
package abac
// PolicyEffect is the outcome a matching policy produces.
type PolicyEffect string
const (
EffectAllow PolicyEffect = "allow"
EffectDeny PolicyEffect = "deny"
)
// Policy defines an attribute-based access control policy.
type Policy struct {
Name string
// A typed effect, not a bare string: a policy written with "Allow" or
// "permit" would silently behave as deny and never be noticed.
Effect PolicyEffect
SubjectConditions map[string]any
ResourceConditions map[string]any
EnvironmentConditions map[string]any
}
// Engine evaluates ABAC policies.
type Engine struct {
policies []Policy
}
// AddPolicy registers a new policy.
func (e *Engine) AddPolicy(p Policy) {
e.policies = append(e.policies, p)
}
// IsAllowed evaluates access request against all policies.
func (e *Engine) IsAllowed(subject, resource map[string]any, action string, env map[string]any) bool {
for _, p := range e.policies {
if matchesConditions(subject, p.SubjectConditions) &&
matchesConditions(resource, p.ResourceConditions) &&
matchesConditions(env, p.EnvironmentConditions) {
return p.Effect == EffectAllow
}
}
// Default deny — an unmatched request is never granted.
return false
}
func matchesConditions(attrs, conditions map[string]any) bool {
for key, expected := range conditions {
val, ok := attrs[key]
if !ok {
return false
}
// Support slice of allowed values
if expectedSlice, ok := expected.([]any); ok {
found := false
for _, e := range expectedSlice {
if val == e {
found = true
break
}
}
if !found {
return false
}
} else if val != expected {
return false
}
}
return true
}
namespace App.Security.Abac;
public enum PolicyEffect
{
Allow,
Deny,
}
public sealed record AbacPolicy(
string Name,
PolicyEffect Effect,
IReadOnlyDictionary<string, object> SubjectConditions,
IReadOnlyDictionary<string, object> ResourceConditions,
IReadOnlyDictionary<string, object> EnvironmentConditions);
public sealed class AbacEngine
{
private readonly List<AbacPolicy> _policies = [];
public void AddPolicy(AbacPolicy policy) => _policies.Add(policy);
// Evaluate an access request against all policies.
public bool IsAllowed(
IReadOnlyDictionary<string, object> subject,
IReadOnlyDictionary<string, object> resource,
string action,
IReadOnlyDictionary<string, object>? environment = null)
{
environment ??= new Dictionary<string, object>();
foreach (AbacPolicy policy in _policies)
{
if (Matches(subject, policy.SubjectConditions)
&& Matches(resource, policy.ResourceConditions)
&& Matches(environment, policy.EnvironmentConditions))
{
return policy.Effect == PolicyEffect.Allow;
}
}
// Default deny — an unmatched request is never granted.
return false;
}
private static bool Matches(
IReadOnlyDictionary<string, object> attributes,
IReadOnlyDictionary<string, object> conditions)
{
foreach ((string key, object expected) in conditions)
{
if (!attributes.TryGetValue(key, out object? actual))
{
return false;
}
// A collection condition means "any of these values".
bool matched = expected is IEnumerable<object> allowed
? allowed.Contains(actual)
: Equals(actual, expected);
if (!matched)
{
return false;
}
}
return true;
}
}
from collections.abc import Collection, Mapping
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
class PolicyEffect(StrEnum):
ALLOW = "allow"
DENY = "deny"
@dataclass(frozen=True, slots=True)
class AbacPolicy:
name: str
effect: PolicyEffect
subject_conditions: Mapping[str, Any]
resource_conditions: Mapping[str, Any]
environment_conditions: Mapping[str, Any]
class AbacEngine:
def __init__(self) -> None:
self._policies: list[AbacPolicy] = []
def add_policy(self, policy: AbacPolicy) -> None:
self._policies.append(policy)
def is_allowed(
self,
subject: Mapping[str, Any],
resource: Mapping[str, Any],
action: str,
environment: Mapping[str, Any] | None = None,
) -> bool:
"""Evaluate an access request against all policies."""
environment = environment or {}
for policy in self._policies:
if (
_matches(subject, policy.subject_conditions)
and _matches(resource, policy.resource_conditions)
and _matches(environment, policy.environment_conditions)
):
return policy.effect is PolicyEffect.ALLOW
# Default deny — an unmatched request is never granted.
return False
def _matches(attributes: Mapping[str, Any], conditions: Mapping[str, Any]) -> bool:
for key, expected in conditions.items():
if key not in attributes:
return False
actual = attributes[key]
# A collection condition means "any of these values".
if isinstance(expected, Collection) and not isinstance(expected, (str, bytes)):
if actual not in expected:
return False
elif actual != 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 (документы, проекты, команды).