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

131 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### Короткий план единой системы доступа (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`.
- Предсказуемые коды ошибок и ответы.
- Уменьшение дублирования загрузки моделей.