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