Files
pipu-music/docs/auth-and-roles.md
T
mrleo1nid f1528de948
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m17s
ci / ts / ts (packages + web) (push) Successful in 3m13s
ci / api / api (push) Successful in 59s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m31s
ci / db / db (push) Successful in 2m50s
feat(admin): transfer user accounts and show local cover coverage
2026-10-23 02:17:06 +03:00

11 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, если вход был по временному паролю).

Перенос пользователей через файл

Администратору на странице «Пользователи» доступны «Экспорт пользователей» и «Импорт пользователей». JSON-файл версии 1 содержит логины, отображаемые имена, роли, дополнительные права аудита загрузок и управления обновлениями, флаги блокировки и обязательной смены пароля, а также хэши Argon2id. Действующие пароли сохраняются; открытые пароли в файле отсутствуют. Файл содержит секретные данные и должен храниться в защищённом месте. Экспорт охватывает всех пользователей независимо от фильтров страницы.

Импорт добавляет отсутствующие логины с новыми ID. Существующие логины пропускаются без изменений пароля, роли и данных пользователя; список пропущенных логинов показывается в результате. Весь файл проверяется до записи, добавление выполняется атомарно. Ограничения: 10000 пользователей, 16 MiB; параметры импортированных хэшей ограничены для безопасной проверки пароля при следующем входе. Рекомендации, история, лайки, плейлисты, сессии и настройки интерфейса не экспортируются и не импортируются. Язык новых записей берётся из настроек сервера. Импорт и экспорт пишутся в журнал без хэшей паролей. API: GET /admin/users/export, POST /admin/users/import (оба только для admin).

Пароли

  • 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.