Files
pipu-music/docs/auth-and-roles.md
T
mrleo1nidandClaude Opus 5.5 9eb1c25147 docs: add product idea, design prototypes and development documentation
- IDEA.md: product vision from the user's perspective (input for design)
- Architecture, ADRs, data model, REST API overview
- Auth and roles, library storage (NFS, path regexes, scanner, search),
  playback, recommendation system ("My Wave"), track upload queue
- Web, Android (offline, self-update), i18n, design system
- Testing strategy, Gitea Actions CI/CD, deployment, conventions, roadmap
- Clickable web/Android design prototypes and wave button variants
- .claude/launch.json to serve the prototypes locally

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:21:31 +03:00

89 lines
7.2 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` | — | — | ✅ |
| Удалить пользователя | — | — | ✅ |
| Хранилища, сканирование, переиндексация | — | — | ✅ |
| Статус библиотеки (только просмотр) | — | ✅ | ✅ |
| Предлагать треки (загрузка в очередь), видеть свои предложения | — | ✅ | ✅ |
| Рассматривать очередь, одобрять / отклонять, править метаданные | — | — | ✅ |
| Лимит очереди, целевое хранилище, шаблон раскладки | — | — | ✅ |
| Статус рекомендательной системы, аудит | — | — | ✅ |
Реализация: в 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`; не совпадает с логином; проверка по
встроенному списку 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`.
Повторное предъявление уже использованного токена ⇒ компрометация ⇒ отзыв всей семьи.
- **Мгновенный отзыв** (блокировка, смена роли, смена пароля): у пользователя инкрементируется `token_version`
(в Redis-кэше), middleware сравнивает с `ver` в access-токене.
- Ключи подписи: `JWT_PRIVATE_KEY` из env / файла; поддержка `kid` для ротации ключей.
- 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`.