mnemo_cards/mnemo_cards_backend/ACCESS_CONTROL_PLAN.md
2025-11-16 14:25:27 +03:00

6.8 KiB
Raw Blame History

Короткий план единой системы доступа (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, buy
    • AdminAction: 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) → 401
  • denied(not_found_mask) → 404
  • denied(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.
  • Предсказуемые коды ошибок и ответы.
  • Уменьшение дублирования загрузки моделей.