Files
pipu-music/docs/auth-and-roles.md
T
mrleo1nid a29a8b18ad
ci / api / api (push) Successful in 52s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m9s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m35s
release / release (версия и заметки) (push) Successful in 5s
release / android (APK в релиз) (push) Successful in 4m6s
ci / db / db (push) Successful in 1m15s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m19s
ci / ts / ts (packages + web) (push) Successful in 2m34s
release / desktop (Linux и Windows в релиз) (push) Successful in 14m27s
release / prune (старые файлы и прогоны) (push) Successful in 6s
feat(admin): manage server updates and local configuration
2026-10-22 23:01:36 +03:00

103 lines
9.1 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.
# Аутентификация, роли, первый запуск
## Роли
| Роль | Назначение |
|---|---|
| `admin` | Владелец системы. Может быть несколько, но минимум один всегда (последнего админа нельзя понизить / удалить / заблокировать). |
| `moderator` | Заместитель администратора: управление обычными пользователями. |
| `user` | Слушатель. |
## Матрица прав
| Действие | user | moderator | admin |
|---|:-:|:-:|:-:|
| Слушать, поиск, коллекция, волна | ✅ | ✅ | ✅ |
| Сменить свой пароль, язык, сессии | ✅ | ✅ | ✅ |
| Открыть админку | — | ✅ | ✅ |
| Видеть список пользователей | — | ✅ | ✅ |
| Создать пользователя с ролью `user` | — | ✅ | ✅ |
| Сбросить пароль / рекомендации, заблокировать `user` | — | ✅ | ✅ |
| Создать `moderator` / `admin`, менять роли | — | — | ✅ |
| Менять логин любого пользователя, включая себя | — | — | ✅ |
| Сбросить пароль / рекомендации, заблокировать `moderator` | — | — | ✅ |
| Удалить пользователя | — | — | ✅ |
| Хранилища, сканирование, переиндексация | — | — | ✅ |
| Статус библиотеки (только просмотр) | — | ✅ | ✅ |
| Предлагать треки (загрузка в очередь), видеть свои предложения | — | ✅ | ✅ |
| Лимит очереди, целевое хранилище, шаблон раскладки | — | — | ✅ |
| Статус рекомендательной системы, аудит | — | — | ✅ |
| Выдать / забрать аудит загрузок | — | — | ✅ |
**Аудит загрузок** (рассматривать очередь, одобрять / отклонять, править метаданные) — не строка матрицы, а
отдельный флаг пользователя `reviews_uploads`, не зависящий от роли: его может иметь кто угодно, сразу несколько
человек, а у админа его может не быть. Проверка — `Principal.Allowed(PermReviewUploads)`.
Реализация: в Go — политики в `internal/auth/policy.go` вида `CanManageUser(actor, target, action) bool`,
проверяются в сервисном слое (не только в middleware), покрываются табличными тестами по этой матрице.
Все админские действия пишутся в `audit_log`.
## Первый запуск (bootstrap)
1. При старте `api` проверяет `app_settings['bootstrap.completed']`. Если нет и в `users` нет ни одного админа —
создаёт пользователя:
- `username = BOOTSTRAP_ADMIN_USERNAME` (по умолчанию `admin`);
- пароль = `BOOTSTRAP_ADMIN_PASSWORD` из env, а если не задан — генерируется случайный (20 символов)
и **печатается в лог один раз**;
- `must_change_credentials = true`.
2. `GET /server-info` возвращает `bootstrap_required: true` — клиент может показать подсказку на экране входа.
3. После входа с `must_change_credentials = true` любые эндпоинты, кроме `/me`, `/me/bootstrap-credentials`,
`/auth/logout`, отвечают `403 { code: "auth.credentials_change_required" }`. Клиент показывает
экран «Смените логин и пароль администратора».
4. `POST /me/bootstrap-credentials` — новый логин (может остаться `admin`, но пароль обязан отличаться),
новый пароль (≥ 12 символов для админа) → флаг снимается, `bootstrap.completed = true`,
все прочие токены отзываются, выдаётся новая пара.
Аналогично для `must_change_password` (после сброса пароля админом / модератором): разрешён только
`POST /me/password` (без `current_password`, если вход был по временному паролю).
## Пароли
- argon2id: `m=64 MiB, t=3, p=2`, соль 16 байт; параметры в хэше (PHC), при изменении — rehash при входе.
- Политика: ≥ 8 символов для `user`, ≥ 12 для `moderator`/`admin`; не совпадает с логином; проверка по
встроенному списку частых паролей (`internal/auth/common_passwords.txt`; пока ~150 записей, цель — 10k).
- Временный пароль при создании пользователя и при сбросе генерирует сервер: 12 символов, показывается **один раз**;
у пользователя ставится `must_change_password = true`.
- Защита от перебора: после 5 неудачных попыток — `locked_until = now() + 5 мин` (растёт экспоненциально до 1 ч),
плюс rate limit по IP в Redis. Ответ всегда одинаковый (`auth.invalid_credentials`) — не раскрываем,
существует ли логин. Время ответа выравнивается (хэш с фиктивным паролем для несуществующих).
## Токены
| | Access | Refresh |
|---|---|---|
| Формат | JWT, EdDSA (Ed25519) | 32 случайных байта, base64url; в БД — SHA-256 |
| Срок | 15 мин | 30 дней скользящий (каждая ротация продлевает), абсолютный максимум 180 дней |
| Claims | `sub`, `role`, `sid` (family_id), `ver` (token_version пользователя), `exp`, `iat` | — |
| Веб | в памяти (JS) | cookie `pipu_rt`: `HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth` |
| Android | в памяти | `expo-secure-store` |
- **Ротация**: каждый `/auth/refresh` помечает старый токен `used_at` и выдаёт новый в той же `family_id`.
Повторное предъявление уже использованного токена ⇒ компрометация ⇒ отзыв всей семьи
(`401 auth.refresh_reused`). Исключение — первые 10 с после ротации: так бывает, когда две вкладки
обновляют токен одновременно; ответ `401 auth.refresh_race` без отзыва, клиент повторяет запрос
с уже новой cookie.
- **Мгновенный отзыв** (блокировка, смена роли, смена пароля): у пользователя инкрементируется `token_version`
(в Redis-кэше), middleware сравнивает с `ver` в access-токене.
- Ключи подписи: `JWT_PRIVATE_KEY` (base64 seed Ed25519) из env; `kid` = первые 8 байт SHA-256 публичного ключа,
прежние ключи для проверки — `JWT_PREVIOUS_KEYS`.
- Смена пароля и учётных данных отзывает все сессии и сразу выдаёт новую пару текущему устройству —
пользователь остаётся в системе, остальные устройства разлогиниваются.
- CSRF: refresh-cookie `SameSite=Strict` и путь только `/api/v1/auth`; остальные запросы — Bearer-заголовок,
поэтому CSRF к ним неприменим.
## Клиентская логика
- Один общий модуль в `packages/core/auth`: хранит access в памяти, при `401 auth.token_expired`
делает **единственный** параллельный refresh (single-flight) и повторяет запросы.
- Роутинг-гарды: `must_change_credentials` → `/setup/admin`; `must_change_password` → `/setup/password`;
админские разделы — по `role`.
Управление обновлениями сервера — право `manages_updates`: у admin всегда, другим его выдаёт и отзывает только admin. Оно даёт доступ к `/admin/updates`, управлению источниками и запуску обновления, но не к конфигурации сервера. `/admin/config` доступен только admin. Подробности: [server-management.md](server-management.md).