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>
8.5 KiB
8.5 KiB
Аутентификация, роли, первый запуск
Роли
| Роль | Назначение |
|---|---|
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)
- При старте
apiпроверяетapp_settings['bootstrap.completed']. Если нет и вusersнет ни одного админа — создаёт пользователя:username = BOOTSTRAP_ADMIN_USERNAME(по умолчаниюadmin);- пароль =
BOOTSTRAP_ADMIN_PASSWORDиз env, а если не задан — генерируется случайный (20 символов) и печатается в лог один раз; must_change_credentials = true.
GET /server-infoвозвращаетbootstrap_required: true— клиент может показать подсказку на экране входа.- После входа с
must_change_credentials = trueлюбые эндпоинты, кроме/me,/me/bootstrap-credentials,/auth/logout, отвечают403 { code: "auth.credentials_change_required" }. Клиент показывает экран «Смените логин и пароль администратора». 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.