mnemo_cards/mnemo_cards_backend/ACCESS_CONTROL_PLAN.md

132 lines
6.8 KiB
Markdown
Raw Normal View History

2025-11-16 11:25:27 +00:00
### Короткий план единой системы доступа (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)
```dart
// 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`.
- Предсказуемые коды ошибок и ответы.
- Уменьшение дублирования загрузки моделей.