From 74da457362355d637e469b070c82deac280313b6 Mon Sep 17 00:00:00 2001 From: mrleo1nid <43583385+mrleo1nid@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:07:05 +0300 Subject: [PATCH] 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 --- api/openapi.yaml | 16 +- apps/web/src/app/router.tsx | 4 +- apps/web/src/pages/admin/UsersPage.tsx | 21 +- apps/web/src/test/fakeApi.ts | 17 + apps/web/src/widgets/AppShell.tsx | 30 +- docs/admin-guide.md | 12 +- docs/auth-and-roles.md | 6 +- docs/uploads.md | 13 +- packages/api-client/src/generated/schema.ts | 8 + packages/core/src/auth.test.ts | 1 + packages/i18n/locales/en/admin.json | 7 +- packages/i18n/locales/ru/admin.json | 7 +- services/api/internal/api/gen/api.gen.go | 549 +++++++++--------- services/api/internal/auth/admin.go | 29 +- services/api/internal/auth/admin_handler.go | 6 +- services/api/internal/auth/admin_test.go | 25 + .../api/internal/auth/authtest/memstore.go | 3 + services/api/internal/auth/handler.go | 1 + services/api/internal/auth/model.go | 2 + services/api/internal/auth/pgstore.go | 14 +- services/api/internal/auth/policy.go | 12 +- services/api/internal/auth/policy_test.go | 9 +- services/api/internal/auth/service.go | 3 +- services/api/internal/auth/store.go | 9 +- services/api/internal/uploads/handler.go | 2 +- services/api/internal/uploads/service.go | 8 +- .../api/migrations/00012_upload_review.sql | 8 + 27 files changed, 503 insertions(+), 319 deletions(-) create mode 100644 services/api/migrations/00012_upload_review.sql diff --git a/api/openapi.yaml b/api/openapi.yaml index 00a3df3..868f573 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -2176,7 +2176,7 @@ components: type: boolean Me: type: object - required: [id, username, display_name, role, locale, ui_prefs, must_change_password, must_change_credentials, created_at] + required: [id, username, display_name, role, reviews_uploads, locale, ui_prefs, must_change_password, must_change_credentials, created_at] properties: id: type: string @@ -2188,6 +2188,9 @@ components: nullable: true role: $ref: "#/components/schemas/Role" + reviews_uploads: + type: boolean + description: Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу locale: type: string ui_prefs: @@ -2228,7 +2231,7 @@ components: type: string AdminUser: type: object - required: [id, username, display_name, role, locale, is_blocked, must_change_password, must_change_credentials, created_at, last_login_at] + required: [id, username, display_name, role, reviews_uploads, locale, is_blocked, must_change_password, must_change_credentials, created_at, last_login_at] properties: id: type: string @@ -2240,6 +2243,9 @@ components: nullable: true role: $ref: "#/components/schemas/Role" + reviews_uploads: + type: boolean + description: Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу locale: type: string is_blocked: @@ -2277,6 +2283,9 @@ components: maxLength: 64 role: $ref: "#/components/schemas/Role" + reviews_uploads: + type: boolean + description: Сразу выдать аудит загрузок. Может задать только админ CreatedUser: type: object required: [user, temporary_password] @@ -2297,6 +2306,9 @@ components: $ref: "#/components/schemas/Role" is_blocked: type: boolean + reviews_uploads: + type: boolean + description: Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя TemporaryPassword: type: object required: [temporary_password] diff --git a/apps/web/src/app/router.tsx b/apps/web/src/app/router.tsx index aba05b3..38be27b 100644 --- a/apps/web/src/app/router.tsx +++ b/apps/web/src/app/router.tsx @@ -226,7 +226,9 @@ const myUploadsRoute = createRoute({ getParentRoute: () => appRoute, path: '/upl const uploadQueueRoute = createRoute({ getParentRoute: () => appRoute, path: '/admin/uploads', - beforeLoad: adminOnly, + beforeLoad: ({ context }) => { + if (!context.user.reviews_uploads) throw redirect({ to: '/' }); + }, component: UploadQueuePage, }); diff --git a/apps/web/src/pages/admin/UsersPage.tsx b/apps/web/src/pages/admin/UsersPage.tsx index 4092c75..a8f128b 100644 --- a/apps/web/src/pages/admin/UsersPage.tsx +++ b/apps/web/src/pages/admin/UsersPage.tsx @@ -132,6 +132,11 @@ export function UsersPage() { label: t('users.make_role', { role: t(`common:roles.${r}`) }), onSelect: () => patch.mutate({ id: u.id, body: { role: r } }), }); + if (me.role === 'admin') + items.push({ + label: t(u.reviews_uploads ? 'users.revoke_review' : 'users.grant_review'), + onSelect: () => patch.mutate({ id: u.id, body: { reviews_uploads: !u.reviews_uploads } }), + }); if (can('delete')) items.push({ label: t('users.delete'), danger: true, onSelect: () => setConfirm({ kind: 'delete', user: u }) }); return items; @@ -206,6 +211,7 @@ export function UsersPage() { {t(`common:roles.${u.role}`)} + {u.reviews_uploads && {t('users.review_uploads')}} {t(`users.status_${st}`)} @@ -285,12 +291,18 @@ function CreateUserDialog({ const [username, setUsername] = useState(''); const [name, setName] = useState(''); const [role, setRole] = useState('user'); + const [reviews, setReviews] = useState(false); const create = useMutation({ mutationFn: async () => unwrap( await session.client.POST('/api/v1/admin/users', { - body: { username: username.trim(), display_name: name.trim() || undefined, role }, + body: { + username: username.trim(), + display_name: name.trim() || undefined, + role, + reviews_uploads: actorRole === 'admin' && reviews ? true : undefined, + }, }), ), onSuccess: (r) => onCreated(r.user.username, r.temporary_password), @@ -342,6 +354,13 @@ function CreateUserDialog({ )} + {actorRole === 'admin' && ( + + )} + {actorRole === 'admin' &&

{t('create.review_uploads_hint')}

}

{t('create.password_note')}

{create.error && !usernameError && !nameError ? errorText(create.error) : null} diff --git a/apps/web/src/test/fakeApi.ts b/apps/web/src/test/fakeApi.ts index b9339e7..276ca51 100644 --- a/apps/web/src/test/fakeApi.ts +++ b/apps/web/src/test/fakeApi.ts @@ -56,6 +56,7 @@ export function makeUser(username: string, role: Role, extra: Partial is_blocked: false, must_change_password: false, must_change_credentials: false, + reviews_uploads: false, created_at: '2026-01-01T00:00:00Z', last_login_at: null, password: `${username}-password`, @@ -80,6 +81,7 @@ export function fakeApi(initial: FakeUser[]) { username: u.username, display_name: u.display_name, role: u.role, + reviews_uploads: u.reviews_uploads, locale: u.locale, ui_prefs: prefs.get(u.id) ?? {}, must_change_password: u.must_change_password, @@ -179,6 +181,7 @@ export function fakeApi(initial: FakeUser[]) { display_name: (body?.display_name as string) ?? null, must_change_password: true, password: 'Temp-Pass-123', + reviews_uploads: body?.reviews_uploads === true && user.role === 'admin', }); users.set(u.id, u); return json({ user: admin(u), temporary_password: u.password }, 201); @@ -235,6 +238,20 @@ export function fakeApi(initial: FakeUser[]) { } if (req.method === 'POST' && /^\/api\/v1\/admin\/users\/[^/]+\/reset-recommendations$/.test(path)) return new Response(null, { status: 204 }); + const userPath = /^\/api\/v1\/admin\/users\/([^/]+)$/.exec(path); + if (userPath && req.method === 'PATCH') { + if (!user || user.role === 'user') return problem(403, 'auth.forbidden'); + const target = users.get(userPath[1]!); + if (!target) return problem(404, 'common.not_found'); + if (typeof body?.reviews_uploads === 'boolean') { + if (user.role !== 'admin') return problem(403, 'auth.forbidden'); + target.reviews_uploads = body.reviews_uploads; + } + if (typeof body?.role === 'string' && user.role === 'admin') target.role = body.role as Role; + if (typeof body?.is_blocked === 'boolean') target.is_blocked = body.is_blocked; + users.set(target.id, target); + return json(admin(target)); + } const reactionPath = /^\/api\/v1\/me\/reactions\/tracks\/([^/]+)$/.exec(path); if (reactionPath) { if (req.method === 'PUT') reactions.set(reactionPath[1]!, body?.reaction as 'like' | 'dislike'); diff --git a/apps/web/src/widgets/AppShell.tsx b/apps/web/src/widgets/AppShell.tsx index fcf884b..88fbc83 100644 --- a/apps/web/src/widgets/AppShell.tsx +++ b/apps/web/src/widgets/AppShell.tsx @@ -153,7 +153,8 @@ function Shell() { const searchAt = user.ui_prefs.search_position ?? 'center'; const search = ; const counts = useCollectionCounts(); - const pending = usePendingUploads(user.role === 'admin'); + const reviews = user.reviews_uploads; + const pending = usePendingUploads(reviews); const unseen = useUnseenUploads(staff); return ( @@ -207,8 +208,17 @@ function Shell() { count={counts.dislikes} /> + {!staff && reviews && ( + + )} {staff && ( - + {user.role === 'admin' && ( @@ -216,15 +226,17 @@ function Shell() { + {reviews && ( + + )} {user.role === 'admin' && ( <> - diff --git a/docs/admin-guide.md b/docs/admin-guide.md index d8298f3..2502ad3 100644 --- a/docs/admin-guide.md +++ b/docs/admin-guide.md @@ -20,7 +20,13 @@ |---|---| | Пользователь | Слушать, искать, собирать коллекцию, «Моя волна» | | Модератор | Плюс: заводить пользователей и сбрасывать им пароли, смотреть статус библиотеки, предлагать треки | -| Администратор | Всё: хранилища, сканы, очередь на добавление, настройки сервера, роли, аудит | +| Администратор | Всё: хранилища, сканы, настройки сервера, роли, аудит | + +**Аудит загрузок** — отдельное право, не роль: рассматривать очередь на добавление (одобрять, отклонять, править +метаданные). Выдаёт и забирает только администратор — при создании пользователя или в меню строки +(«Дать / Забрать аудит загрузок»), в том числе себе. Право может быть у нескольких людей сразу и у любой роли; +у администратора его может не быть. Первый (bootstrap) администратор и админы, существовавшие до появления права, +получают его автоматически. Полная матрица прав — [auth-and-roles.md](auth-and-roles.md#матрица-прав). @@ -57,8 +63,8 @@ ## 4. Очередь на добавление -Модераторы и администраторы предлагают треки («Предложить треки»), администратор решает, что попадёт -в библиотеку. Подробно — [uploads.md](uploads.md). +Модераторы и администраторы предлагают треки («Предложить треки»), что попадёт в библиотеку, решают люди +с правом **аудита загрузок**. Подробно — [uploads.md](uploads.md). - **Куда пишутся файлы:** у хранилища включите «Принимать загрузки» и задайте **шаблон раскладки** — «Подобрать» предложит шаблон по уже лежащим файлам, «Проверить» убедится, что из пути, построенного шаблоном, сканер diff --git a/docs/auth-and-roles.md b/docs/auth-and-roles.md index f1c11f2..4a2c7d3 100644 --- a/docs/auth-and-roles.md +++ b/docs/auth-and-roles.md @@ -24,9 +24,13 @@ | Хранилища, сканирование, переиндексация | — | — | ✅ | | Статус библиотеки (только просмотр) | — | ✅ | ✅ | | Предлагать треки (загрузка в очередь), видеть свои предложения | — | ✅ | ✅ | -| Рассматривать очередь, одобрять / отклонять, править метаданные | — | — | ✅ | | Лимит очереди, целевое хранилище, шаблон раскладки | — | — | ✅ | | Статус рекомендательной системы, аудит | — | — | ✅ | +| Выдать / забрать аудит загрузок | — | — | ✅ | + +**Аудит загрузок** (рассматривать очередь, одобрять / отклонять, править метаданные) — не строка матрицы, а +отдельный флаг пользователя `reviews_uploads`, не зависящий от роли: его может иметь кто угодно, сразу несколько +человек, а у админа его может не быть. Проверка — `Principal.Allowed(PermReviewUploads)`. Реализация: в Go — политики в `internal/auth/policy.go` вида `CanManageUser(actor, target, action) bool`, проверяются в сервисном слое (не только в middleware), покрываются табличными тестами по этой матрице. diff --git a/docs/uploads.md b/docs/uploads.md index 5f0bf2d..4715284 100644 --- a/docs/uploads.md +++ b/docs/uploads.md @@ -1,7 +1,7 @@ # Предложение треков (загрузка с модерацией) Модераторы (и админы) предлагают новые треки. Файлы попадают в **очередь на добавление** с общим лимитом размера, -дубли отбрасываются сразу, админ одобряет или отклоняет, одобренные файлы раскладываются в хранилище +дубли отбрасываются сразу, аудитор загрузок одобряет или отклоняет, одобренные файлы раскладываются в хранилище **по той же структуре папок, что принята в этом хранилище**. ## 1. Роли @@ -10,10 +10,13 @@ |---|:-:|:-:|:-:| | Загрузить треки в очередь | — | ✅ | ✅ | | Видеть свои предложения и их статус, отозвать до рассмотрения | — | ✅ | ✅ | -| Видеть всю очередь | — | — | ✅ | -| Править метаданные, слушать, одобрять / отклонять | — | — | ✅ | | Настраивать лимит очереди, целевое хранилище и шаблон раскладки | — | — | ✅ | +Видеть всю очередь, слушать, править метаданные, одобрять и отклонять может только тот, у кого есть право +**аудита загрузок** (`users.reviews_uploads`, в API — `reviews_uploads` в `Me` / `AdminUser`). Это не роль: право +выдаёт админ, оно может быть у нескольких людей сразу, у модератора или даже у обычного пользователя, а у админа +его может не быть. Без него админ по-прежнему настраивает лимиты и хранилища, но очередь не видит. + Загрузки админа проходят ту же проверку на дубли; в очередь они тоже попадают, но админ может одобрить их сразу. ## 2. Жизненный цикл загрузки @@ -142,7 +145,7 @@ - Если файл по целевому пути уже существует — добавляется суффикс ` (2)`, а в задаче — предупреждение. - Путь проверяется на выход за корень хранилища (`..`, абсолютные пути) — ошибка. -## 8. Рассмотрение (админ) +## 8. Рассмотрение (аудит загрузок) Экран «Очередь на добавление»: - Группировка по пачке загрузки / по альбому; фильтры: статус, автор, `possible_duplicate`. @@ -173,7 +176,7 @@ ## 10. Уведомления - Автор видит статусы в «Мои предложения» (раздел админки модератора), бейдж с числом решений с последнего визита. -- Админ видит бейдж «N на рассмотрении» в сайдбаре. +- Аудитор загрузок видит бейдж «N на рассмотрении» в сайдбаре. ## Реализация (`services/api/internal/uploads`) diff --git a/packages/api-client/src/generated/schema.ts b/packages/api-client/src/generated/schema.ts index c770c8a..ea11439 100644 --- a/packages/api-client/src/generated/schema.ts +++ b/packages/api-client/src/generated/schema.ts @@ -1648,6 +1648,8 @@ export interface components { username: string; display_name: string | null; role: components["schemas"]["Role"]; + /** @description Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу */ + reviews_uploads: boolean; locale: string; ui_prefs: components["schemas"]["UiPrefs"]; must_change_password: boolean; @@ -1675,6 +1677,8 @@ export interface components { username: string; display_name: string | null; role: components["schemas"]["Role"]; + /** @description Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу */ + reviews_uploads: boolean; locale: string; is_blocked: boolean; must_change_password: boolean; @@ -1692,6 +1696,8 @@ export interface components { username: string; display_name?: string; role: components["schemas"]["Role"]; + /** @description Сразу выдать аудит загрузок. Может задать только админ */ + reviews_uploads?: boolean; }; CreatedUser: { user: components["schemas"]["AdminUser"]; @@ -1703,6 +1709,8 @@ export interface components { display_name?: string; role?: components["schemas"]["Role"]; is_blocked?: boolean; + /** @description Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя */ + reviews_uploads?: boolean; }; TemporaryPassword: { temporary_password: string; diff --git a/packages/core/src/auth.test.ts b/packages/core/src/auth.test.ts index c921301..3130c44 100644 --- a/packages/core/src/auth.test.ts +++ b/packages/core/src/auth.test.ts @@ -11,6 +11,7 @@ const user = { ui_prefs: {}, must_change_password: false, must_change_credentials: false, + reviews_uploads: false, created_at: '2026-01-01T00:00:00Z', }; diff --git a/packages/i18n/locales/en/admin.json b/packages/i18n/locales/en/admin.json index 6dccff5..c70956a 100644 --- a/packages/i18n/locales/en/admin.json +++ b/packages/i18n/locales/en/admin.json @@ -1,7 +1,7 @@ { "users": { "title": "Users", - "subtitle": "Create users and moderators, manage roles, reset passwords, block accounts", + "subtitle": "Create users and moderators, manage roles, upload review, reset passwords, block accounts", "create": "Create user", "search": "Name or username", "all": "All", @@ -20,6 +20,9 @@ "block": "Block", "unblock": "Unblock", "make_role": "Make {{role}}", + "review_uploads": "Upload review", + "grant_review": "Grant upload review", + "revoke_review": "Revoke upload review", "delete": "Delete", "delete_confirm": "Delete {{username}}? Their history and likes will be lost.", "block_confirm": "Block {{username}}? All their sessions will end.", @@ -36,6 +39,8 @@ "username_hint": "Latin letters, digits, \".\", \"_\", \"-\"", "display_name": "Name", "role": "Role", + "review_uploads": "Upload review", + "review_uploads_hint": "They can review the upload queue. This is not a role: several people can have it, and an admin might not.", "password_note": "The server will generate a password and show it once.", "submit": "Create" }, diff --git a/packages/i18n/locales/ru/admin.json b/packages/i18n/locales/ru/admin.json index 41f431f..50b88f6 100644 --- a/packages/i18n/locales/ru/admin.json +++ b/packages/i18n/locales/ru/admin.json @@ -1,7 +1,7 @@ { "users": { "title": "Пользователи", - "subtitle": "Создание пользователей и модераторов, роли, сброс пароля, блокировка", + "subtitle": "Создание пользователей и модераторов, роли, аудит загрузок, сброс пароля, блокировка", "create": "Создать пользователя", "search": "Имя или логин", "all": "Все", @@ -20,6 +20,9 @@ "block": "Заблокировать", "unblock": "Разблокировать", "make_role": "Сделать: {{role}}", + "review_uploads": "Аудит загрузок", + "grant_review": "Дать аудит загрузок", + "revoke_review": "Забрать аудит загрузок", "delete": "Удалить", "delete_confirm": "Удалить {{username}}? Его история и лайки пропадут.", "block_confirm": "Заблокировать {{username}}? Все его сессии завершатся.", @@ -36,6 +39,8 @@ "username_hint": "Латинские буквы, цифры, «.», «_», «-»", "display_name": "Имя", "role": "Роль", + "review_uploads": "Аудит загрузок", + "review_uploads_hint": "Сможет рассматривать очередь. Это не роль: можно выдать нескольким людям, у админа её может не быть.", "password_note": "Пароль сгенерирует сервер и покажет один раз.", "submit": "Создать" }, diff --git a/services/api/internal/api/gen/api.gen.go b/services/api/internal/api/gen/api.gen.go index dec407d..9753cf0 100644 --- a/services/api/internal/api/gen/api.gen.go +++ b/services/api/internal/api/gen/api.gen.go @@ -241,8 +241,11 @@ type AdminUser struct { Locale string `json:"locale"` MustChangeCredentials bool `json:"must_change_credentials"` MustChangePassword bool `json:"must_change_password"` - Role Role `json:"role"` - Username string `json:"username"` + + // ReviewsUploads Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу + ReviewsUploads bool `json:"reviews_uploads"` + Role Role `json:"role"` + Username string `json:"username"` } // AdminUserPage defines model for AdminUserPage. @@ -388,8 +391,11 @@ type ClientType string // CreateUserRequest defines model for CreateUserRequest. type CreateUserRequest struct { DisplayName *string `json:"display_name,omitempty"` - Role Role `json:"role"` - Username string `json:"username"` + + // ReviewsUploads Сразу выдать аудит загрузок. Может задать только админ + ReviewsUploads *bool `json:"reviews_uploads,omitempty"` + Role Role `json:"role"` + Username string `json:"username"` } // CreatedUser defines model for CreatedUser. @@ -493,9 +499,12 @@ type Me struct { Locale string `json:"locale"` MustChangeCredentials bool `json:"must_change_credentials"` MustChangePassword bool `json:"must_change_password"` - Role Role `json:"role"` - UiPrefs UiPrefs `json:"ui_prefs"` - Username string `json:"username"` + + // ReviewsUploads Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу + ReviewsUploads bool `json:"reviews_uploads"` + Role Role `json:"role"` + UiPrefs UiPrefs `json:"ui_prefs"` + Username string `json:"username"` } // Onboarding defines model for Onboarding. @@ -962,7 +971,10 @@ type UpdateUserRequest struct { // DisplayName Пустая строка — сбросить имя DisplayName *string `json:"display_name,omitempty"` IsBlocked *bool `json:"is_blocked,omitempty"` - Role *Role `json:"role,omitempty"` + + // ReviewsUploads Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя + ReviewsUploads *bool `json:"reviews_uploads,omitempty"` + Role *Role `json:"role,omitempty"` } // UploadBatchCreated defines model for UploadBatchCreated. @@ -9789,265 +9801,268 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/services/api/internal/auth/admin.go b/services/api/internal/auth/admin.go index 248fda2..bea588d 100644 --- a/services/api/internal/auth/admin.go +++ b/services/api/internal/auth/admin.go @@ -75,9 +75,10 @@ func (s *Service) ListUsers(ctx context.Context, p Principal, query string, role // CreateUserInput — форма создания пользователя. type CreateUserInput struct { - Username string - DisplayName string - Role Role + Username string + DisplayName string + Role Role + ReviewsUploads bool } // CreateUser создаёт пользователя с временным паролем, который возвращается один раз. @@ -88,6 +89,9 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu if !CanAssignRole(p.Role, in.Role) || !CanManageUser(actorOf(p), Actor{Role: in.Role}, UserCreate) { return User{}, "", forbidden() } + if in.ReviewsUploads && p.Role != RoleAdmin { + return User{}, "", forbidden() + } username := NormalizeUsername(in.Username) if err := ValidateUsername("username", username); err != nil { return User{}, "", err @@ -109,7 +113,8 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu locale = s.cfg.SupportedLocales[0] } u := User{ - ID: s.ids.New(), Username: username, DisplayName: name, PasswordHash: hash, Role: in.Role, Locale: locale, + ID: s.ids.New(), Username: username, DisplayName: name, PasswordHash: hash, Role: in.Role, + ReviewsUploads: in.ReviewsUploads, Locale: locale, MustChangePassword: true, CreatedAt: s.clock.Now(), } err = s.store.WithTx(ctx, func(tx Store) error { @@ -118,7 +123,7 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu } return tx.InsertAudit(ctx, s.ids.New(), AuditEntry{ ActorID: &p.UserID, Action: "user.create", TargetType: "user", TargetID: &u.ID, - Payload: map[string]any{"username": username, "role": string(in.Role)}, + Payload: map[string]any{"username": username, "role": string(in.Role), "reviews_uploads": in.ReviewsUploads}, }) }) if errors.Is(err, ErrUsernameTaken) { @@ -145,9 +150,10 @@ func displayName(s string) (*string, error) { // UserPatch — изменения пользователя из админки. nil — не менять. type UserPatch struct { - DisplayName *string - Role *Role - IsBlocked *bool + DisplayName *string + Role *Role + IsBlocked *bool + ReviewsUploads *bool } // UpdateUser меняет имя, роль и блокировку с проверкой прав по каждому изменению. @@ -191,6 +197,13 @@ func (s *Service) UpdateUser(ctx context.Context, p Principal, id uuid.UUID, pat upd.Role, upd.RevokeTokens = patch.Role, true audit("user.role_change", map[string]any{"from": string(target.Role), "to": string(*patch.Role)}) } + if patch.ReviewsUploads != nil && *patch.ReviewsUploads != target.ReviewsUploads { + if p.Role != RoleAdmin { + return forbidden() + } + upd.ReviewsUploads = patch.ReviewsUploads + audit("user.reviews_uploads", map[string]any{"reviews_uploads": *patch.ReviewsUploads}) + } blockChange := patch.IsBlocked != nil && *patch.IsBlocked != target.IsBlocked if blockChange { if !CanManageUser(actor, tgt, UserBlock) { diff --git a/services/api/internal/auth/admin_handler.go b/services/api/internal/auth/admin_handler.go index 407f153..782d442 100644 --- a/services/api/internal/auth/admin_handler.go +++ b/services/api/internal/auth/admin_handler.go @@ -44,6 +44,9 @@ func (h *Handler) CreateUser(ctx context.Context, req gen.CreateUserRequestObjec return nil, err } in := CreateUserInput{Username: req.Body.Username, Role: Role(req.Body.Role)} + if req.Body.ReviewsUploads != nil { + in.ReviewsUploads = *req.Body.ReviewsUploads + } if req.Body.DisplayName != nil { in.DisplayName = *req.Body.DisplayName } @@ -60,7 +63,7 @@ func (h *Handler) UpdateUser(ctx context.Context, req gen.UpdateUserRequestObjec if err != nil { return nil, err } - patch := UserPatch{DisplayName: req.Body.DisplayName, IsBlocked: req.Body.IsBlocked} + patch := UserPatch{DisplayName: req.Body.DisplayName, IsBlocked: req.Body.IsBlocked, ReviewsUploads: req.Body.ReviewsUploads} if req.Body.Role != nil { r := Role(*req.Body.Role) patch.Role = &r @@ -147,6 +150,7 @@ func AdminUserDTO(u User) gen.AdminUser { Username: u.Username, DisplayName: u.DisplayName, Role: gen.Role(u.Role), + ReviewsUploads: u.ReviewsUploads, Locale: u.Locale, IsBlocked: u.IsBlocked, MustChangePassword: u.MustChangePassword, diff --git a/services/api/internal/auth/admin_test.go b/services/api/internal/auth/admin_test.go index 123c2eb..fe6a3e6 100644 --- a/services/api/internal/auth/admin_test.go +++ b/services/api/internal/auth/admin_test.go @@ -395,3 +395,28 @@ func TestAudit(t *testing.T) { mod := e.signIn("mod", "moderator-pass-1") assert.Equal(t, auth.CodeForbidden, e.do(http.MethodGet, "/api/v1/admin/audit", nil, bearer(mod)).code()) } + +// Аудит загрузок выдаёт только админ, и он не следует из роли. +func TestReviewUploadsGrant(t *testing.T) { + e := newEnv(t) + e.addUser("root", "root-password-1", auth.RoleAdmin) + admin := e.signIn("root", "root-password-1") + target := e.addUser("mod", "mod-password-1", auth.RoleModerator) + mod := e.signIn("mod", "mod-password-1") + path := "/api/v1/admin/users/" + target.ID.String() + grant := map[string]any{"reviews_uploads": true} + + me := e.do(http.MethodGet, "/api/v1/me", nil, bearer(admin)) + assert.Equal(t, false, me.body["reviews_uploads"]) + + assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, path, grant, bearer(mod)).code()) + assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPost, "/api/v1/admin/users", + map[string]any{"username": "rev", "role": "user", "reviews_uploads": true}, bearer(mod)).code()) + + r := e.do(http.MethodPatch, path, grant, bearer(admin)) + require.Equal(t, http.StatusOK, r.Code, r.Body.String()) + assert.Equal(t, true, r.body["reviews_uploads"]) + assert.Equal(t, "moderator", r.body["role"]) + me = e.do(http.MethodGet, "/api/v1/me", nil, bearer(mod)) + assert.Equal(t, true, me.body["reviews_uploads"]) +} diff --git a/services/api/internal/auth/authtest/memstore.go b/services/api/internal/auth/authtest/memstore.go index f01d0d1..bdb17e9 100644 --- a/services/api/internal/auth/authtest/memstore.go +++ b/services/api/internal/auth/authtest/memstore.go @@ -303,6 +303,9 @@ func (s *MemStore) UpdateUser(ctx context.Context, id uuid.UUID, upd auth.UserUp if upd.IsBlocked != nil { u.IsBlocked = *upd.IsBlocked } + if upd.ReviewsUploads != nil { + u.ReviewsUploads = *upd.ReviewsUploads + } if upd.RevokeTokens { u.TokenVersion++ } diff --git a/services/api/internal/auth/handler.go b/services/api/internal/auth/handler.go index 80a5fa4..425e1f4 100644 --- a/services/api/internal/auth/handler.go +++ b/services/api/internal/auth/handler.go @@ -231,6 +231,7 @@ func MeDTO(u User) gen.Me { Username: u.Username, DisplayName: u.DisplayName, Role: gen.Role(u.Role), + ReviewsUploads: u.ReviewsUploads, Locale: u.Locale, UiPrefs: uiPrefsDTO(u.UIPrefs), MustChangePassword: u.MustChangePassword, diff --git a/services/api/internal/auth/model.go b/services/api/internal/auth/model.go index c0d663c..5709da5 100644 --- a/services/api/internal/auth/model.go +++ b/services/api/internal/auth/model.go @@ -54,6 +54,7 @@ type User struct { DisplayName *string PasswordHash string Role Role + ReviewsUploads bool // аудит очереди на добавление; не следует из роли Locale string UIPrefs UIPrefs MustChangePassword bool @@ -94,6 +95,7 @@ type Session struct { type Principal struct { UserID uuid.UUID Role Role + ReviewsUploads bool SessionID uuid.UUID MustChangePassword bool MustChangeCredentials bool diff --git a/services/api/internal/auth/pgstore.go b/services/api/internal/auth/pgstore.go index 7f79565..68c7312 100644 --- a/services/api/internal/auth/pgstore.go +++ b/services/api/internal/auth/pgstore.go @@ -39,7 +39,8 @@ func (s *PgStore) WithTx(ctx context.Context, fn func(Store) error) error { } const userColumns = `id, username, display_name, password_hash, role, locale, ui_prefs, must_change_password, - must_change_credentials, is_blocked, token_version, failed_login_count, locked_until, created_at, last_login_at` + must_change_credentials, is_blocked, token_version, failed_login_count, locked_until, created_at, last_login_at, + reviews_uploads` func scanUser(row pgx.Row) (User, error) { var ( @@ -49,7 +50,7 @@ func scanUser(row pgx.Row) (User, error) { ) err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &u.PasswordHash, &role, &u.Locale, &prefs, &u.MustChangePassword, &u.MustChangeCredentials, &u.IsBlocked, &u.TokenVersion, &u.FailedLoginCount, - &u.LockedUntil, &u.CreatedAt, &u.LastLoginAt) + &u.LockedUntil, &u.CreatedAt, &u.LastLoginAt, &u.ReviewsUploads) if errors.Is(err, pgx.ErrNoRows) { return User{}, ErrNotFound } @@ -81,10 +82,10 @@ func (s *PgStore) CreateUser(ctx context.Context, u User, createdBy *uuid.UUID) } _, err = s.q.Exec(ctx, ` INSERT INTO users (id, username, display_name, password_hash, role, locale, ui_prefs, - must_change_password, must_change_credentials, created_by, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11)`, + must_change_password, must_change_credentials, created_by, created_at, updated_at, reviews_uploads) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11, $12)`, u.ID, u.Username, u.DisplayName, u.PasswordHash, string(u.Role), u.Locale, prefs, - u.MustChangePassword, u.MustChangeCredentials, createdBy, u.CreatedAt) + u.MustChangePassword, u.MustChangeCredentials, createdBy, u.CreatedAt, u.ReviewsUploads) if isUniqueViolation(err) { return ErrUsernameTaken } @@ -344,10 +345,11 @@ func (s *PgStore) UpdateUser(ctx context.Context, id uuid.UUID, upd UserUpdate) display_name = CASE WHEN $2 THEN $3 ELSE display_name END, role = COALESCE($4::user_role, role), is_blocked = COALESCE($5, is_blocked), + reviews_uploads = COALESCE($7, reviews_uploads), token_version = token_version + CASE WHEN $6 THEN 1 ELSE 0 END, updated_at = now() WHERE id = $1 - RETURNING `+userColumns, id, setName, name, role, upd.IsBlocked, upd.RevokeTokens)) + RETURNING `+userColumns, id, setName, name, role, upd.IsBlocked, upd.RevokeTokens, upd.ReviewsUploads)) } // ResetPassword реализует Store. diff --git a/services/api/internal/auth/policy.go b/services/api/internal/auth/policy.go index 3b0a545..a368c7f 100644 --- a/services/api/internal/auth/policy.go +++ b/services/api/internal/auth/policy.go @@ -14,7 +14,7 @@ const ( PermManageLibrary // хранилища, сканирование, переиндексация PermViewLibraryStatus // статус библиотеки (просмотр) PermSuggestTracks // предлагать треки, видеть свои предложения - PermReviewUploads // очередь, одобрение, правка метаданных, лимиты + PermReviewUploads // очередь, одобрение, правка метаданных; даёт флаг, не роль PermSystemStatus // статус рекомендательной системы, аудит ) @@ -24,11 +24,11 @@ var permissions = map[Permission][]Role{ PermManageLibrary: {RoleAdmin}, PermViewLibraryStatus: {RoleModerator, RoleAdmin}, PermSuggestTracks: {RoleModerator, RoleAdmin}, - PermReviewUploads: {RoleAdmin}, PermSystemStatus: {RoleAdmin}, } // Can сообщает, есть ли у роли глобальное право. +// Аудит загрузок сюда не входит: его проверяет Principal.Allowed. func Can(r Role, p Permission) bool { for _, allowed := range permissions[p] { if allowed == r { @@ -38,6 +38,14 @@ func Can(r Role, p Permission) bool { return false } +// Allowed — право конкретного пользователя. Аудит загрузок не вытекает из роли admin. +func (p Principal) Allowed(perm Permission) bool { + if perm == PermReviewUploads { + return p.ReviewsUploads + } + return Can(p.Role, perm) +} + // UserAction — действие над учётной записью другого пользователя. type UserAction int diff --git a/services/api/internal/auth/policy_test.go b/services/api/internal/auth/policy_test.go index 17c2be3..a2f0a34 100644 --- a/services/api/internal/auth/policy_test.go +++ b/services/api/internal/auth/policy_test.go @@ -20,7 +20,6 @@ func TestCan(t *testing.T) { {PermManageLibrary, "manage library", [3]bool{false, false, true}}, {PermViewLibraryStatus, "library status", [3]bool{false, true, true}}, {PermSuggestTracks, "suggest tracks", [3]bool{false, true, true}}, - {PermReviewUploads, "review uploads", [3]bool{false, false, true}}, {PermSystemStatus, "system status", [3]bool{false, false, true}}, } roles := [3]Role{RoleUser, RoleModerator, RoleAdmin} @@ -72,6 +71,14 @@ func TestCanManageUserSelf(t *testing.T) { assert.True(t, CanManageUser(admin, admin, UserResetPassword)) } +func TestReviewUploadsIsSeparateFromRole(t *testing.T) { + assert.False(t, Principal{Role: RoleAdmin}.Allowed(PermReviewUploads)) + assert.True(t, Principal{Role: RoleModerator, ReviewsUploads: true}.Allowed(PermReviewUploads)) + assert.True(t, Principal{Role: RoleUser, ReviewsUploads: true}.Allowed(PermReviewUploads)) + assert.False(t, Principal{Role: RoleUser}.Allowed(PermReviewUploads)) + assert.True(t, Principal{Role: RoleUser, ReviewsUploads: true}.Allowed(PermSuggestTracks) == false) +} + func TestCanAssignRole(t *testing.T) { cases := []struct { actor, role Role diff --git a/services/api/internal/auth/service.go b/services/api/internal/auth/service.go index b3955d0..bfaf84d 100644 --- a/services/api/internal/auth/service.go +++ b/services/api/internal/auth/service.go @@ -368,6 +368,7 @@ func (s *Service) Authenticate(ctx context.Context, access string) (Principal, e return Principal{ UserID: u.ID, Role: u.Role, + ReviewsUploads: u.ReviewsUploads, SessionID: claims.SessionID, MustChangePassword: u.MustChangePassword, MustChangeCredentials: u.MustChangeCredentials, @@ -621,7 +622,7 @@ func (s *Service) Bootstrap(ctx context.Context) error { } u := User{ ID: s.ids.New(), Username: username, PasswordHash: hash, Role: RoleAdmin, Locale: "ru", - MustChangeCredentials: true, CreatedAt: s.clock.Now(), + ReviewsUploads: true, MustChangeCredentials: true, CreatedAt: s.clock.Now(), } err = s.store.WithTx(ctx, func(tx Store) error { if err := tx.CreateUser(ctx, u, nil); err != nil { diff --git a/services/api/internal/auth/store.go b/services/api/internal/auth/store.go index 2671127..5590268 100644 --- a/services/api/internal/auth/store.go +++ b/services/api/internal/auth/store.go @@ -31,10 +31,11 @@ type UserFilter struct { // UserUpdate — изменения из админки. nil — не менять. type UserUpdate struct { - DisplayName **string - Role *Role - IsBlocked *bool - RevokeTokens bool + DisplayName **string + Role *Role + IsBlocked *bool + ReviewsUploads *bool + RevokeTokens bool } // Store — доступ к users, refresh_tokens, audit_log и app_settings. diff --git a/services/api/internal/uploads/handler.go b/services/api/internal/uploads/handler.go index 36d293d..fd79d56 100644 --- a/services/api/internal/uploads/handler.go +++ b/services/api/internal/uploads/handler.go @@ -53,7 +53,7 @@ func (h *Handler) fileDTO(ctx context.Context, f File) gen.UploadFile { out.TusUrl = &u } if h.signer != nil && f.StagingPath != "" && f.Status.KeepsStaging() { - if p, ok := auth.PrincipalFrom(ctx); ok && auth.Can(p.Role, auth.PermReviewUploads) { + if p, ok := auth.PrincipalFrom(ctx); ok && p.ReviewsUploads { u := h.streamURL(f.ID) out.StreamUrl = &u } diff --git a/services/api/internal/uploads/service.go b/services/api/internal/uploads/service.go index 9aa42d2..a0cdd9d 100644 --- a/services/api/internal/uploads/service.go +++ b/services/api/internal/uploads/service.go @@ -67,7 +67,7 @@ func principal(ctx context.Context, perm auth.Permission) (auth.Principal, error if !ok { return auth.Principal{}, apperr.Unauthorized(auth.CodeUnauthorized, "not authenticated") } - if !auth.Can(p.Role, perm) { + if !p.Allowed(perm) { return auth.Principal{}, apperr.Forbidden(auth.CodeForbidden, "not allowed") } return p, nil @@ -425,9 +425,9 @@ func (s *Service) Retry(ctx context.Context, id uuid.UUID) (File, error) { return s.store.File(ctx, id) } -// GetSettings — настройки очереди (админ). +// GetSettings — лимиты очереди (админ, не аудитор загрузок). func (s *Service) GetSettings(ctx context.Context) (Settings, error) { - if _, err := principal(ctx, auth.PermReviewUploads); err != nil { + if _, err := principal(ctx, auth.PermManageLibrary); err != nil { return Settings{}, err } return s.store.Settings(ctx) @@ -446,7 +446,7 @@ type SettingsInput struct { // UpdateSettings меняет лимиты; уменьшение ниже занятого ничего не удаляет (docs/uploads.md#3). func (s *Service) UpdateSettings(ctx context.Context, in SettingsInput) (Settings, error) { - p, err := principal(ctx, auth.PermReviewUploads) + p, err := principal(ctx, auth.PermManageLibrary) if err != nil { return Settings{}, err } diff --git a/services/api/migrations/00012_upload_review.sql b/services/api/migrations/00012_upload_review.sql new file mode 100644 index 0000000..bce09a4 --- /dev/null +++ b/services/api/migrations/00012_upload_review.sql @@ -0,0 +1,8 @@ +-- +goose Up +-- Аудит загрузок — отдельное право, не роль. Уже существующие админы его сохраняют, +-- чтобы очередь не осталась без того, кто её рассматривает. Новым админам его выдают явно. +ALTER TABLE users ADD COLUMN reviews_uploads boolean NOT NULL DEFAULT false; +UPDATE users SET reviews_uploads = true WHERE role = 'admin'; + +-- +goose Down +ALTER TABLE users DROP COLUMN reviews_uploads;