6.8 KiB
6.8 KiB
Короткий план единой системы доступа (policy-based authorization)
- Суть: выносим все проверки вида packId/admin/подписка/покупка/enable в единый слой Authorization Policies, а в хендлерах используем декларативные «гарды»/хелперы. В итоге в эндпоинтах остаётся одна строка вида
await access.require(PackAction.view, packId);.
ВАЖНО: Можно удалить v1 ручки!!!
1) Модель прав и ресурсов
- Resource:
Pack,Card,Test,Purchase,AdminArea, … - Actions на ресурсы:
PackAction:list,view,cards,tests,edit,delete,buyAdminAction:access
- AccessContext:
user,request,query,path,now, флаги окружения. - AccessResult:
allowed | denied(reason).
Минимум ENUM’ы и типы в lib/api/authorize/acl_types.dart.
2) Политики (policy layer)
- PackAccessPolicy единственное место истины для всех проверок паков:
isAdmin→ allow all pack actions- публичные правила (например, preview pack id=10 для
view) enabled/наличие в базе → deny с маскировкой 404 для приватных- доступ по покупке/подписке для
view/cards/tests - редактирование/удаление только админам
- Аналогично при необходимости:
CardPolicy,TestPolicy,AdminPolicy.
Реализация в lib/api/authorize/policies/pack_policy.dart и пр.
3) Сервис авторизации
- AccessService координирует политики: роутит ресурс+действие → нужную политику.
- API:
Future<void> require(ResourceAction action, {resourceId})Future<bool> can(ResourceAction action, {resourceId})- Варианты
requirePack(PackAction action, String packId)и т.п.
Файл: lib/api/authorize/access_service.dart.
4) Единая загрузка моделей (resource loader)
- Вынести загрузку моделей и кэширование в ResourceLoader:
getPackModel(packId)(с кэшем на запрос) + флагиexists/enabled.- Позволяет политикам не дергать базу по 3 раза.
- Хранить в
Request.contextдля reuse по всему запросу.
Файл: lib/api/authorize/resource_loader.dart.
5) Middleware
- Уже есть auth middlewares (
appAuthorize,authorizeV2). Добавляем:accessMiddleware: кладётAccessServiceиResourceLoaderвRequest.context.- Оборачиваем и v1, и v2 пайплайны в
mnemo_shelf.dart.
Пример вставки рядом с авторизацией:
- v1:
.addMiddleware(accessMiddleware(getIt.get<AccessService>())) - v2: после
authorizeV2(...)— чтобыuserуже был вrequest.
6) Декларативные гарды/хелперы для роутов
- Набор хелперов, чтобы минимизировать код в хендлерах:
withPack(String packId, Future<Response> Function(PackModel) fn)— загрузка/404.guardPack(String packId, PackAction action)—await access.require(...);- Комбо-хелпер:
withPackGuarded(packId, PackAction.view, (pack) => ...)
Файл: lib/api/authorize/route_guards.dart.
7) Миграция эндпоинтов (пошагово)
- V2 сначала (меньше риска, уже структурированы):
GET /api/v2/packs/<packId>: заменить всеif (packId == '10') ...и проверки подписки наawait access.requirePack(PackAction.view, packId);, а логику «показывать purchased» — оставить как дополнение к ответу.GET /api/v2/packs/<packId>/cards:await access.requirePack(PackAction.cards, packId);GET /api/v2/packs/<packId>/tests:await access.requirePack(PackAction.tests, packId);
- V1 затем:
lib/packs/packs_api.dart: убратьrequest.user == null/packId != '10'/adminпроверки; заменить наaccess.require....- Редактирование/удаление:
await access.require(AdminAction.access)илиawait access.requirePack(PackAction.edit/delete, packId).
Важно: ошибки политики маппятся централизованно:
denied(unauthenticated)→ 401denied(not_found_mask)→ 404denied(forbidden)→ 403- JSON ответ единообразный.
8) Единая конфигурация правил
- Конфиг для публичных паков и особых кейсов:
- Например,
publicPackIds = {'10'}. - Флаги: «разрешить изображения без авторизации для enabled pack».
- Например,
- Файл:
lib/api/authorize/access_config.dart.
9) Трассировка и аудит
- Лёгкий логгер в
AccessService: кто/что/результат. - Опционально: счётчики Prometheus (allowed/denied per policy).
10) Тесты
- Unit-тесты для
PackAccessPolicyс таблицей кейсов:- anon vs user vs admin; enabled vs disabled; purchase vs subscription; public id=10.
- Интеграционные для ключевых эндпоинтов v2.
11) Гайд по использованию для разработчиков
- «В хендлерах не пишем
if (user == null || ...)— толькоaccess.require(...)». - Если нужен объект —
withPackGuarded(packId, action, (pack) => ...). - Любые изменения правил — только в Policy, нигде больше.
Короткий пример (идея API)
// handler
@Route.get('/packs/<packId>/cards')
Future<Response> getPackCards(Request request, String packId) async {
final access = request.access; // из middleware
await access.requirePack(PackAction.cards, packId);
return withPack(packId, (pack) async {
final cards = await loadCards(pack); // бизнес-логика без проверок
return ok({'items': cards});
});
}
Что это даст
- Единая точка правил: меньше багов, проще изменять доступ.
- Читабельные хендлеры без разрозненных
if. - Предсказуемые коды ошибок и ответы.
- Уменьшение дублирования загрузки моделей.