Опирается на правила:
AUTH-10…AUTH-12из Auth Patterns Style Guide → раздел 4. ABAC: владение ресурсом.
Важно знать
- ABAC обязателен для каждой команды/запроса, работающего с агрегатом по id.
- Два способа:
@PreAuthorize("@access.canEditOrder(#id, authentication.principal)")или проверка внутри Handler.- ABAC-логика — в
@Component("access")бине или в Handler. Не размазывается по контроллерам.- Роль
adminобходит ABAC (полный доступ), но каждое действие пишется в audit log.- Без ABAC — RBAC-only endpoint становится IDOR (Insecure Direct Object Reference) уязвимостью.
- Любой
GET /orders/{id}без ownership-check позволяет user-42 читать заказы user-99.
ABAC (Attribute-Based Access Control) — второй слой авторизации. RBAC говорит «роль customer может вызывать GET /orders/{id}», ABAC говорит «но этот customer может читать только свои orders». Без ABAC любой holder JWT с правильной ролью получает доступ ко всем ресурсам — классический IDOR.
Два способа
AUTH-10: @PreAuthorize-бин или handler-check.
Способ 1: @access бин для простых случаев
@Component("access")
@RequiredArgsConstructor
public class AccessChecker {
private final OrderRepository orderRepository;
public boolean canEditOrder(Long orderId, Object principal) {
var userId = Long.valueOf(principal.toString());
return orderRepository.findById(orderId)
.map(order -> order.getCustomerId().equals(userId))
.orElse(false);
}
public boolean canViewOrder(Long orderId, Object principal) {
return canEditOrder(orderId, principal);
}
}
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
@PostMapping("/{id}/cancel")
@PreAuthorize("hasAnyRole('customer', 'admin')"
+ " and (hasRole('admin') or @access.canEditOrder(#id, authentication.name))")
public OrderResponse cancel(@PathVariable Long id) {
return dispatcher.dispatch(new CancelOrderCommand(id));
}
}
hasRole('admin') — admin обходит ABAC.
Хороший подход для простых ownership checks (один-к-одному, никакой бизнес-логики кроме сравнения).
Способ 2: проверка внутри Handler
@UseCase
@RequiredArgsConstructor
public class CancelOrderHandler implements UseCaseHandler<CancelOrderCommand, Order> {
private final OrderRepository orderRepository;
private final AuthenticatedUserProvider userProvider;
@Override
@Transactional
public Order handle(CancelOrderCommand command) {
var order = orderRepository.findById(command.orderId(), SelectMode.FOR_UPDATE)
.orElseThrow(() -> new OrderNotFoundException(command.orderId()));
var user = userProvider.current();
if (!user.isAdmin() && !order.getCustomerId().equals(user.id())) {
throw new ForbiddenException("Order does not belong to current user");
}
if (!order.canCancel()) {
throw new OrderCannotBeCancelledException(order.id(), order.status());
}
order.cancel();
return orderRepository.save(order);
}
}
Хороший подход для случаев со сложной логикой: проверка не только ownership, но и состояния, бизнес-правил.
Когда какой способ
| Случай | Способ |
|---|---|
Простая ownership order.customerId == jwt.sub | @access бин |
| Composite: ownership + ещё одно условие в state | Handler-check |
| Reading endpoints, где данные потом не модифицируются | @access бин |
| Write endpoints, где надо FOR UPDATE load + check | Handler-check |
| Multi-aggregate проверка (admin or seller of related product) | Handler-check |
Главное — один из двух, никогда оба одновременно. Дублирование проверок ведёт к расхождению (изменили в одном, забыли в другом).
ABAC централизована, не размазана
AUTH-11: правила про размещение.
Корректно:
@access.canEditOrder— единый бин, единая логика ownership для Order.- Все ABAC-методы Order в одном месте — легко найти, легко изменить.
Неверно:
// ПЛОХО — ABAC размазан по контроллерам
@PostMapping("/{id}/cancel")
@PreAuthorize("hasRole('customer')")
public OrderResponse cancel(@PathVariable Long id, Authentication auth) {
var order = orderRepository.findById(id).orElseThrow();
if (!order.getCustomerId().toString().equals(auth.getName())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return dispatcher.dispatch(new CancelOrderCommand(id));
}
Что не так:
- Логика проверки дублируется в каждом endpoint (read, update, cancel).
- Контроллер делает бизнес-проверку (нарушение принципа thin controller).
- При изменении модели (например, co-ownership) надо обновлять N мест.
Корректно: централизация в @Component("access") или в Handler через AccessChecker.
Admin обходит ABAC + audit
AUTH-12: admin может всё, но след остаётся.
@UseCase
@RequiredArgsConstructor
public class CancelOrderHandler implements UseCaseHandler<CancelOrderCommand, Order> {
private final OrderRepository orderRepository;
private final AuthenticatedUserProvider userProvider;
private final AdminAuditLog auditLog;
@Override
@Transactional
public Order handle(CancelOrderCommand command) {
var order = orderRepository.findById(command.orderId(), SelectMode.FOR_UPDATE)
.orElseThrow(() -> new OrderNotFoundException(command.orderId()));
var user = userProvider.current();
if (!user.isAdmin() && !order.getCustomerId().equals(user.id())) {
throw new ForbiddenException("Order does not belong to current user");
}
order.cancel();
var saved = orderRepository.save(order);
if (user.isAdmin()) {
auditLog.record(AdminAction.builder()
.actorId(user.id())
.action("cancel-order")
.resourceType("Order")
.resourceId(order.id())
.occurredAt(Instant.now())
.metadata(Map.of("originalStatus", order.previousStatus().name()))
.build());
}
return saved;
}
}
Admin может отменить чужой заказ (support, compliance, ops-задачи), но в *_audit_log остаётся запись «admin-7 cancelled order-12345 в момент X». Это покрывает compliance, расследование инцидентов, audit от security team.
Подробнее — Audit admin-команд.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| Endpoint с RBAC, но без ABAC для own-resource | AUTH-10 | оба слоя обязательны |
| ABAC проверка в контроллере inline | AUTH-11 | @access бин или Handler |
Дубликат ABAC в @PreAuthorize + Handler | AUTH-11 | один из способов |
| Admin без audit log при override | AUTH-12 | auditLog.record(...) обязательно |
ABAC проверяет только customerId == sub, игнорирует admin | AUTH-12 | user.isAdmin() OR ownership |
@access.canEditOrder без FOR UPDATE для write | AUTH-10 | use Handler для write с lock |
ABAC падает с RuntimeException, не ForbiddenException | AUTH-6 | typed exception → 403 |
| ABAC игнорирует deleted/inactive ресурсы | AUTH-10 | check status + ownership |
Куда дальше
- Auth → раздел 4. ABAC — нормативные формулировки.
- RBAC: маппинг ролей — слой до ABAC.
- Где какая проверка — ABAC в Domain, не в Gateway.
- Audit admin-команд — обязательная пара к admin-override.
- Error handling → exception hierarchy —
ForbiddenException. - DDD → aggregate — ownership-fields в агрегате.