Files
pipu-music/docs/auth-and-roles.md
T
mrleo1nidandClaude Opus 5.5 74da457362 feat(auth): make upload review a separate grant instead of an admin privilege
Reviewing the upload queue is now a per-user flag (reviews_uploads), not part
of the admin role: several people can hold it, a moderator can have it, and an
admin may not. Only admins grant or revoke it; existing admins keep it.

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

8.5 KiB
Raw Permalink Blame History

Аутентификация, роли, первый запуск

Роли

Роль Назначение
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.