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>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-21 10:07:05 +03:00
1 parent 066ba24834
commit 74da457362
27 files changed
+503 -319

No files matched your search

+14 -2
View File
@@ -2176,7 +2176,7 @@ components:
type: boolean type: boolean
Me: Me:
type: object 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: properties:
id: id:
type: string type: string
@@ -2188,6 +2188,9 @@ components:
nullable: true nullable: true
role: role:
$ref: "#/components/schemas/Role" $ref: "#/components/schemas/Role"
reviews_uploads:
type: boolean
description: Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу
locale: locale:
type: string type: string
ui_prefs: ui_prefs:
@@ -2228,7 +2231,7 @@ components:
type: string type: string
AdminUser: AdminUser:
type: object 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: properties:
id: id:
type: string type: string
@@ -2240,6 +2243,9 @@ components:
nullable: true nullable: true
role: role:
$ref: "#/components/schemas/Role" $ref: "#/components/schemas/Role"
reviews_uploads:
type: boolean
description: Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу
locale: locale:
type: string type: string
is_blocked: is_blocked:
@@ -2277,6 +2283,9 @@ components:
maxLength: 64 maxLength: 64
role: role:
$ref: "#/components/schemas/Role" $ref: "#/components/schemas/Role"
reviews_uploads:
type: boolean
description: Сразу выдать аудит загрузок. Может задать только админ
CreatedUser: CreatedUser:
type: object type: object
required: [user, temporary_password] required: [user, temporary_password]
@@ -2297,6 +2306,9 @@ components:
$ref: "#/components/schemas/Role" $ref: "#/components/schemas/Role"
is_blocked: is_blocked:
type: boolean type: boolean
reviews_uploads:
type: boolean
description: Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя
TemporaryPassword: TemporaryPassword:
type: object type: object
required: [temporary_password] required: [temporary_password]
+3 -1
View File
@@ -226,7 +226,9 @@ const myUploadsRoute = createRoute({ getParentRoute: () => appRoute, path: '/upl
const uploadQueueRoute = createRoute({ const uploadQueueRoute = createRoute({
getParentRoute: () => appRoute, getParentRoute: () => appRoute,
path: '/admin/uploads', path: '/admin/uploads',
beforeLoad: adminOnly, beforeLoad: ({ context }) => {
if (!context.user.reviews_uploads) throw redirect({ to: '/' });
},
component: UploadQueuePage, component: UploadQueuePage,
}); });
+20 -1
View File
@@ -132,6 +132,11 @@ export function UsersPage() {
label: t('users.make_role', { role: t(`common:roles.${r}`) }), label: t('users.make_role', { role: t(`common:roles.${r}`) }),
onSelect: () => patch.mutate({ id: u.id, body: { role: 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')) if (can('delete'))
items.push({ label: t('users.delete'), danger: true, onSelect: () => setConfirm({ kind: 'delete', user: u }) }); items.push({ label: t('users.delete'), danger: true, onSelect: () => setConfirm({ kind: 'delete', user: u }) });
return items; return items;
@@ -206,6 +211,7 @@ export function UsersPage() {
</td> </td>
<td> <td>
<span className={cx(s.badge, s[`role_${u.role}`])}>{t(`common:roles.${u.role}`)}</span> <span className={cx(s.badge, s[`role_${u.role}`])}>{t(`common:roles.${u.role}`)}</span>
{u.reviews_uploads && <span className={cx(s.badge, s.role_moderator)}>{t('users.review_uploads')}</span>}
</td> </td>
<td> <td>
<span className={cx(s.status, s[`status_${st}`])}>{t(`users.status_${st}`)}</span> <span className={cx(s.status, s[`status_${st}`])}>{t(`users.status_${st}`)}</span>
@@ -285,12 +291,18 @@ function CreateUserDialog({
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [name, setName] = useState(''); const [name, setName] = useState('');
const [role, setRole] = useState<Role>('user'); const [role, setRole] = useState<Role>('user');
const [reviews, setReviews] = useState(false);
const create = useMutation({ const create = useMutation({
mutationFn: async () => mutationFn: async () =>
unwrap( unwrap(
await session.client.POST('/api/v1/admin/users', { 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), onSuccess: (r) => onCreated(r.user.username, r.temporary_password),
@@ -342,6 +354,13 @@ function CreateUserDialog({
</select> </select>
</label> </label>
)} )}
{actorRole === 'admin' && (
<label className={s.sessions} style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<input type="checkbox" checked={reviews} onChange={(e) => setReviews(e.target.checked)} />
<span>{t('create.review_uploads')}</span>
</label>
)}
{actorRole === 'admin' && <p className={s.note}>{t('create.review_uploads_hint')}</p>}
<p className={s.note}>{t('create.password_note')}</p> <p className={s.note}>{t('create.password_note')}</p>
<FormError>{create.error && !usernameError && !nameError ? errorText(create.error) : null}</FormError> <FormError>{create.error && !usernameError && !nameError ? errorText(create.error) : null}</FormError>
</form> </form>
+17
View File
@@ -56,6 +56,7 @@ export function makeUser(username: string, role: Role, extra: Partial<FakeUser>
is_blocked: false, is_blocked: false,
must_change_password: false, must_change_password: false,
must_change_credentials: false, must_change_credentials: false,
reviews_uploads: false,
created_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
last_login_at: null, last_login_at: null,
password: `${username}-password`, password: `${username}-password`,
@@ -80,6 +81,7 @@ export function fakeApi(initial: FakeUser[]) {
username: u.username, username: u.username,
display_name: u.display_name, display_name: u.display_name,
role: u.role, role: u.role,
reviews_uploads: u.reviews_uploads,
locale: u.locale, locale: u.locale,
ui_prefs: prefs.get(u.id) ?? {}, ui_prefs: prefs.get(u.id) ?? {},
must_change_password: u.must_change_password, must_change_password: u.must_change_password,
@@ -179,6 +181,7 @@ export function fakeApi(initial: FakeUser[]) {
display_name: (body?.display_name as string) ?? null, display_name: (body?.display_name as string) ?? null,
must_change_password: true, must_change_password: true,
password: 'Temp-Pass-123', password: 'Temp-Pass-123',
reviews_uploads: body?.reviews_uploads === true && user.role === 'admin',
}); });
users.set(u.id, u); users.set(u.id, u);
return json({ user: admin(u), temporary_password: u.password }, 201); 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)) if (req.method === 'POST' && /^\/api\/v1\/admin\/users\/[^/]+\/reset-recommendations$/.test(path))
return new Response(null, { status: 204 }); 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); const reactionPath = /^\/api\/v1\/me\/reactions\/tracks\/([^/]+)$/.exec(path);
if (reactionPath) { if (reactionPath) {
if (req.method === 'PUT') reactions.set(reactionPath[1]!, body?.reaction as 'like' | 'dislike'); if (req.method === 'PUT') reactions.set(reactionPath[1]!, body?.reaction as 'like' | 'dislike');
+21 -9
View File
@@ -153,7 +153,8 @@ function Shell() {
const searchAt = user.ui_prefs.search_position ?? 'center'; const searchAt = user.ui_prefs.search_position ?? 'center';
const search = <SearchBox align={searchAt} collapsible={user.ui_prefs.search_collapse === true} />; const search = <SearchBox align={searchAt} collapsible={user.ui_prefs.search_collapse === true} />;
const counts = useCollectionCounts(); const counts = useCollectionCounts();
const pending = usePendingUploads(user.role === 'admin'); const reviews = user.reviews_uploads;
const pending = usePendingUploads(reviews);
const unseen = useUnseenUploads(staff); const unseen = useUnseenUploads(staff);
return ( return (
@@ -207,8 +208,17 @@ function Shell() {
count={counts.dislikes} count={counts.dislikes}
/> />
</NavSection> </NavSection>
{!staff && reviews && (
<NavLink
to="/admin/uploads"
label={t('nav.upload_queue')}
icon={inboxIcon}
activeIcon={inboxFillIcon}
badge={pending}
/>
)}
{staff && ( {staff && (
<NavSection id="admin" title={t('nav.admin')} badge={pending} defaultFolded> <NavSection id="admin" title={t('nav.admin')} badge={reviews ? pending : undefined} defaultFolded>
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} /> <NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
{user.role === 'admin' && ( {user.role === 'admin' && (
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} /> <NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
@@ -216,15 +226,17 @@ function Shell() {
<NavLink to="/admin/library" label={t('nav.library')} icon={libraryIcon} activeIcon={libraryFillIcon} /> <NavLink to="/admin/library" label={t('nav.library')} icon={libraryIcon} activeIcon={libraryFillIcon} />
<NavLink to="/uploads/new" label={t('nav.suggest')} icon={uploadIcon} activeIcon={uploadFillIcon} /> <NavLink to="/uploads/new" label={t('nav.suggest')} icon={uploadIcon} activeIcon={uploadFillIcon} />
<NavLink to="/uploads/mine" label={t('nav.my_uploads')} icon={myUploadsIcon} activeIcon={myUploadsFillIcon} badge={unseen} /> <NavLink to="/uploads/mine" label={t('nav.my_uploads')} icon={myUploadsIcon} activeIcon={myUploadsFillIcon} badge={unseen} />
{reviews && (
<NavLink
to="/admin/uploads"
label={t('nav.upload_queue')}
icon={inboxIcon}
activeIcon={inboxFillIcon}
badge={pending}
/>
)}
{user.role === 'admin' && ( {user.role === 'admin' && (
<> <>
<NavLink
to="/admin/uploads"
label={t('nav.upload_queue')}
icon={inboxIcon}
activeIcon={inboxFillIcon}
badge={pending}
/>
<NavLink to="/admin/settings" label={t('nav.admin_settings')} icon={tuneIcon} activeIcon={tuneFillIcon} /> <NavLink to="/admin/settings" label={t('nav.admin_settings')} icon={tuneIcon} activeIcon={tuneFillIcon} />
<NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} /> <NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} />
</> </>
+9 -3
View File
@@ -20,7 +20,13 @@
|---|---| |---|---|
| Пользователь | Слушать, искать, собирать коллекцию, «Моя волна» | | Пользователь | Слушать, искать, собирать коллекцию, «Моя волна» |
| Модератор | Плюс: заводить пользователей и сбрасывать им пароли, смотреть статус библиотеки, предлагать треки | | Модератор | Плюс: заводить пользователей и сбрасывать им пароли, смотреть статус библиотеки, предлагать треки |
| Администратор | Всё: хранилища, сканы, очередь на добавление, настройки сервера, роли, аудит | | Администратор | Всё: хранилища, сканы, настройки сервера, роли, аудит |
**Аудит загрузок** — отдельное право, не роль: рассматривать очередь на добавление (одобрять, отклонять, править
метаданные). Выдаёт и забирает только администратор — при создании пользователя или в меню строки
(«Дать / Забрать аудит загрузок»), в том числе себе. Право может быть у нескольких людей сразу и у любой роли;
у администратора его может не быть. Первый (bootstrap) администратор и админы, существовавшие до появления права,
получают его автоматически.
Полная матрица прав — [auth-and-roles.md](auth-and-roles.md#матрица-прав). Полная матрица прав — [auth-and-roles.md](auth-and-roles.md#матрица-прав).
@@ -57,8 +63,8 @@
## 4. Очередь на добавление ## 4. Очередь на добавление
Модераторы и администраторы предлагают треки («Предложить треки»), администратор решает, что попадёт Модераторы и администраторы предлагают треки («Предложить треки»), что попадёт в библиотеку, решают люди
в библиотеку. Подробно — [uploads.md](uploads.md). с правом **аудита загрузок**. Подробно — [uploads.md](uploads.md).
- **Куда пишутся файлы:** у хранилища включите «Принимать загрузки» и задайте **шаблон раскладки** — «Подобрать» - **Куда пишутся файлы:** у хранилища включите «Принимать загрузки» и задайте **шаблон раскладки** — «Подобрать»
предложит шаблон по уже лежащим файлам, «Проверить» убедится, что из пути, построенного шаблоном, сканер предложит шаблон по уже лежащим файлам, «Проверить» убедится, что из пути, построенного шаблоном, сканер
+5 -1
View File
@@ -24,9 +24,13 @@
| Хранилища, сканирование, переиндексация | — | — | ✅ | | Хранилища, сканирование, переиндексация | — | — | ✅ |
| Статус библиотеки (только просмотр) | — | ✅ | ✅ | | Статус библиотеки (только просмотр) | — | ✅ | ✅ |
| Предлагать треки (загрузка в очередь), видеть свои предложения | — | ✅ | ✅ | | Предлагать треки (загрузка в очередь), видеть свои предложения | — | ✅ | ✅ |
| Рассматривать очередь, одобрять / отклонять, править метаданные | — | — | ✅ |
| Лимит очереди, целевое хранилище, шаблон раскладки | — | — | ✅ | | Лимит очереди, целевое хранилище, шаблон раскладки | — | — | ✅ |
| Статус рекомендательной системы, аудит | — | — | ✅ | | Статус рекомендательной системы, аудит | — | — | ✅ |
| Выдать / забрать аудит загрузок | — | — | ✅ |
**Аудит загрузок** (рассматривать очередь, одобрять / отклонять, править метаданные) — не строка матрицы, а
отдельный флаг пользователя `reviews_uploads`, не зависящий от роли: его может иметь кто угодно, сразу несколько
человек, а у админа его может не быть. Проверка — `Principal.Allowed(PermReviewUploads)`.
Реализация: в Go — политики в `internal/auth/policy.go` вида `CanManageUser(actor, target, action) bool`, Реализация: в Go — политики в `internal/auth/policy.go` вида `CanManageUser(actor, target, action) bool`,
проверяются в сервисном слое (не только в middleware), покрываются табличными тестами по этой матрице. проверяются в сервисном слое (не только в middleware), покрываются табличными тестами по этой матрице.
+8 -5
View File
@@ -1,7 +1,7 @@
# Предложение треков (загрузка с модерацией) # Предложение треков (загрузка с модерацией)
Модераторы (и админы) предлагают новые треки. Файлы попадают в **очередь на добавление** с общим лимитом размера, Модераторы (и админы) предлагают новые треки. Файлы попадают в **очередь на добавление** с общим лимитом размера,
дубли отбрасываются сразу, админ одобряет или отклоняет, одобренные файлы раскладываются в хранилище дубли отбрасываются сразу, аудитор загрузок одобряет или отклоняет, одобренные файлы раскладываются в хранилище
**по той же структуре папок, что принята в этом хранилище**. **по той же структуре папок, что принята в этом хранилище**.
## 1. Роли ## 1. Роли
@@ -10,10 +10,13 @@
|---|:-:|:-:|:-:| |---|:-:|:-:|:-:|
| Загрузить треки в очередь | — | ✅ | ✅ | | Загрузить треки в очередь | — | ✅ | ✅ |
| Видеть свои предложения и их статус, отозвать до рассмотрения | — | ✅ | ✅ | | Видеть свои предложения и их статус, отозвать до рассмотрения | — | ✅ | ✅ |
| Видеть всю очередь | — | — | ✅ |
| Править метаданные, слушать, одобрять / отклонять | — | — | ✅ |
| Настраивать лимит очереди, целевое хранилище и шаблон раскладки | — | — | ✅ | | Настраивать лимит очереди, целевое хранилище и шаблон раскладки | — | — | ✅ |
Видеть всю очередь, слушать, править метаданные, одобрять и отклонять может только тот, у кого есть право
**аудита загрузок** (`users.reviews_uploads`, в API — `reviews_uploads` в `Me` / `AdminUser`). Это не роль: право
выдаёт админ, оно может быть у нескольких людей сразу, у модератора или даже у обычного пользователя, а у админа
его может не быть. Без него админ по-прежнему настраивает лимиты и хранилища, но очередь не видит.
Загрузки админа проходят ту же проверку на дубли; в очередь они тоже попадают, но админ может одобрить их сразу. Загрузки админа проходят ту же проверку на дубли; в очередь они тоже попадают, но админ может одобрить их сразу.
## 2. Жизненный цикл загрузки ## 2. Жизненный цикл загрузки
@@ -142,7 +145,7 @@
- Если файл по целевому пути уже существует — добавляется суффикс ` (2)`, а в задаче — предупреждение. - Если файл по целевому пути уже существует — добавляется суффикс ` (2)`, а в задаче — предупреждение.
- Путь проверяется на выход за корень хранилища (`..`, абсолютные пути) — ошибка. - Путь проверяется на выход за корень хранилища (`..`, абсолютные пути) — ошибка.
## 8. Рассмотрение (админ) ## 8. Рассмотрение (аудит загрузок)
Экран «Очередь на добавление»: Экран «Очередь на добавление»:
- Группировка по пачке загрузки / по альбому; фильтры: статус, автор, `possible_duplicate`. - Группировка по пачке загрузки / по альбому; фильтры: статус, автор, `possible_duplicate`.
@@ -173,7 +176,7 @@
## 10. Уведомления ## 10. Уведомления
- Автор видит статусы в «Мои предложения» (раздел админки модератора), бейдж с числом решений с последнего визита. - Автор видит статусы в «Мои предложения» (раздел админки модератора), бейдж с числом решений с последнего визита.
- Админ видит бейдж «N на рассмотрении» в сайдбаре. - Аудитор загрузок видит бейдж «N на рассмотрении» в сайдбаре.
## Реализация (`services/api/internal/uploads`) ## Реализация (`services/api/internal/uploads`)
@@ -1648,6 +1648,8 @@ export interface components {
username: string; username: string;
display_name: string | null; display_name: string | null;
role: components["schemas"]["Role"]; role: components["schemas"]["Role"];
/** @description Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу */
reviews_uploads: boolean;
locale: string; locale: string;
ui_prefs: components["schemas"]["UiPrefs"]; ui_prefs: components["schemas"]["UiPrefs"];
must_change_password: boolean; must_change_password: boolean;
@@ -1675,6 +1677,8 @@ export interface components {
username: string; username: string;
display_name: string | null; display_name: string | null;
role: components["schemas"]["Role"]; role: components["schemas"]["Role"];
/** @description Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу */
reviews_uploads: boolean;
locale: string; locale: string;
is_blocked: boolean; is_blocked: boolean;
must_change_password: boolean; must_change_password: boolean;
@@ -1692,6 +1696,8 @@ export interface components {
username: string; username: string;
display_name?: string; display_name?: string;
role: components["schemas"]["Role"]; role: components["schemas"]["Role"];
/** @description Сразу выдать аудит загрузок. Может задать только админ */
reviews_uploads?: boolean;
}; };
CreatedUser: { CreatedUser: {
user: components["schemas"]["AdminUser"]; user: components["schemas"]["AdminUser"];
@@ -1703,6 +1709,8 @@ export interface components {
display_name?: string; display_name?: string;
role?: components["schemas"]["Role"]; role?: components["schemas"]["Role"];
is_blocked?: boolean; is_blocked?: boolean;
/** @description Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя */
reviews_uploads?: boolean;
}; };
TemporaryPassword: { TemporaryPassword: {
temporary_password: string; temporary_password: string;
+1
View File
@@ -11,6 +11,7 @@ const user = {
ui_prefs: {}, ui_prefs: {},
must_change_password: false, must_change_password: false,
must_change_credentials: false, must_change_credentials: false,
reviews_uploads: false,
created_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
}; };
+6 -1
View File
@@ -1,7 +1,7 @@
{ {
"users": { "users": {
"title": "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", "create": "Create user",
"search": "Name or username", "search": "Name or username",
"all": "All", "all": "All",
@@ -20,6 +20,9 @@
"block": "Block", "block": "Block",
"unblock": "Unblock", "unblock": "Unblock",
"make_role": "Make {{role}}", "make_role": "Make {{role}}",
"review_uploads": "Upload review",
"grant_review": "Grant upload review",
"revoke_review": "Revoke upload review",
"delete": "Delete", "delete": "Delete",
"delete_confirm": "Delete {{username}}? Their history and likes will be lost.", "delete_confirm": "Delete {{username}}? Their history and likes will be lost.",
"block_confirm": "Block {{username}}? All their sessions will end.", "block_confirm": "Block {{username}}? All their sessions will end.",
@@ -36,6 +39,8 @@
"username_hint": "Latin letters, digits, \".\", \"_\", \"-\"", "username_hint": "Latin letters, digits, \".\", \"_\", \"-\"",
"display_name": "Name", "display_name": "Name",
"role": "Role", "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.", "password_note": "The server will generate a password and show it once.",
"submit": "Create" "submit": "Create"
}, },
+6 -1
View File
@@ -1,7 +1,7 @@
{ {
"users": { "users": {
"title": "Пользователи", "title": "Пользователи",
"subtitle": "Создание пользователей и модераторов, роли, сброс пароля, блокировка", "subtitle": "Создание пользователей и модераторов, роли, аудит загрузок, сброс пароля, блокировка",
"create": "Создать пользователя", "create": "Создать пользователя",
"search": "Имя или логин", "search": "Имя или логин",
"all": "Все", "all": "Все",
@@ -20,6 +20,9 @@
"block": "Заблокировать", "block": "Заблокировать",
"unblock": "Разблокировать", "unblock": "Разблокировать",
"make_role": "Сделать: {{role}}", "make_role": "Сделать: {{role}}",
"review_uploads": "Аудит загрузок",
"grant_review": "Дать аудит загрузок",
"revoke_review": "Забрать аудит загрузок",
"delete": "Удалить", "delete": "Удалить",
"delete_confirm": "Удалить {{username}}? Его история и лайки пропадут.", "delete_confirm": "Удалить {{username}}? Его история и лайки пропадут.",
"block_confirm": "Заблокировать {{username}}? Все его сессии завершатся.", "block_confirm": "Заблокировать {{username}}? Все его сессии завершатся.",
@@ -36,6 +39,8 @@
"username_hint": "Латинские буквы, цифры, «.», «_», «-»", "username_hint": "Латинские буквы, цифры, «.», «_», «-»",
"display_name": "Имя", "display_name": "Имя",
"role": "Роль", "role": "Роль",
"review_uploads": "Аудит загрузок",
"review_uploads_hint": "Сможет рассматривать очередь. Это не роль: можно выдать нескольким людям, у админа её может не быть.",
"password_note": "Пароль сгенерирует сервер и покажет один раз.", "password_note": "Пароль сгенерирует сервер и покажет один раз.",
"submit": "Создать" "submit": "Создать"
}, },
+282 -267
View File
@@ -241,8 +241,11 @@ type AdminUser struct {
Locale string `json:"locale"` Locale string `json:"locale"`
MustChangeCredentials bool `json:"must_change_credentials"` MustChangeCredentials bool `json:"must_change_credentials"`
MustChangePassword bool `json:"must_change_password"` 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. // AdminUserPage defines model for AdminUserPage.
@@ -388,8 +391,11 @@ type ClientType string
// CreateUserRequest defines model for CreateUserRequest. // CreateUserRequest defines model for CreateUserRequest.
type CreateUserRequest struct { type CreateUserRequest struct {
DisplayName *string `json:"display_name,omitempty"` 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. // CreatedUser defines model for CreatedUser.
@@ -493,9 +499,12 @@ type Me struct {
Locale string `json:"locale"` Locale string `json:"locale"`
MustChangeCredentials bool `json:"must_change_credentials"` MustChangeCredentials bool `json:"must_change_credentials"`
MustChangePassword bool `json:"must_change_password"` MustChangePassword bool `json:"must_change_password"`
Role Role `json:"role"`
UiPrefs UiPrefs `json:"ui_prefs"` // ReviewsUploads Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу
Username string `json:"username"` ReviewsUploads bool `json:"reviews_uploads"`
Role Role `json:"role"`
UiPrefs UiPrefs `json:"ui_prefs"`
Username string `json:"username"`
} }
// Onboarding defines model for Onboarding. // Onboarding defines model for Onboarding.
@@ -962,7 +971,10 @@ type UpdateUserRequest struct {
// DisplayName Пустая строка — сбросить имя // DisplayName Пустая строка — сбросить имя
DisplayName *string `json:"display_name,omitempty"` DisplayName *string `json:"display_name,omitempty"`
IsBlocked *bool `json:"is_blocked,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. // 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 // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
"AQA=",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file
+21 -8
View File
@@ -75,9 +75,10 @@ func (s *Service) ListUsers(ctx context.Context, p Principal, query string, role
// CreateUserInput — форма создания пользователя. // CreateUserInput — форма создания пользователя.
type CreateUserInput struct { type CreateUserInput struct {
Username string Username string
DisplayName string DisplayName string
Role Role Role Role
ReviewsUploads bool
} }
// CreateUser создаёт пользователя с временным паролем, который возвращается один раз. // 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) { if !CanAssignRole(p.Role, in.Role) || !CanManageUser(actorOf(p), Actor{Role: in.Role}, UserCreate) {
return User{}, "", forbidden() return User{}, "", forbidden()
} }
if in.ReviewsUploads && p.Role != RoleAdmin {
return User{}, "", forbidden()
}
username := NormalizeUsername(in.Username) username := NormalizeUsername(in.Username)
if err := ValidateUsername("username", username); err != nil { if err := ValidateUsername("username", username); err != nil {
return User{}, "", err return User{}, "", err
@@ -109,7 +113,8 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu
locale = s.cfg.SupportedLocales[0] locale = s.cfg.SupportedLocales[0]
} }
u := User{ 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(), MustChangePassword: true, CreatedAt: s.clock.Now(),
} }
err = s.store.WithTx(ctx, func(tx Store) error { 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{ return tx.InsertAudit(ctx, s.ids.New(), AuditEntry{
ActorID: &p.UserID, Action: "user.create", TargetType: "user", TargetID: &u.ID, 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) { if errors.Is(err, ErrUsernameTaken) {
@@ -145,9 +150,10 @@ func displayName(s string) (*string, error) {
// UserPatch — изменения пользователя из админки. nil — не менять. // UserPatch — изменения пользователя из админки. nil — не менять.
type UserPatch struct { type UserPatch struct {
DisplayName *string DisplayName *string
Role *Role Role *Role
IsBlocked *bool IsBlocked *bool
ReviewsUploads *bool
} }
// UpdateUser меняет имя, роль и блокировку с проверкой прав по каждому изменению. // 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 upd.Role, upd.RevokeTokens = patch.Role, true
audit("user.role_change", map[string]any{"from": string(target.Role), "to": string(*patch.Role)}) 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 blockChange := patch.IsBlocked != nil && *patch.IsBlocked != target.IsBlocked
if blockChange { if blockChange {
if !CanManageUser(actor, tgt, UserBlock) { if !CanManageUser(actor, tgt, UserBlock) {
+5 -1
View File
@@ -44,6 +44,9 @@ func (h *Handler) CreateUser(ctx context.Context, req gen.CreateUserRequestObjec
return nil, err return nil, err
} }
in := CreateUserInput{Username: req.Body.Username, Role: Role(req.Body.Role)} 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 { if req.Body.DisplayName != nil {
in.DisplayName = *req.Body.DisplayName in.DisplayName = *req.Body.DisplayName
} }
@@ -60,7 +63,7 @@ func (h *Handler) UpdateUser(ctx context.Context, req gen.UpdateUserRequestObjec
if err != nil { if err != nil {
return nil, err 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 { if req.Body.Role != nil {
r := Role(*req.Body.Role) r := Role(*req.Body.Role)
patch.Role = &r patch.Role = &r
@@ -147,6 +150,7 @@ func AdminUserDTO(u User) gen.AdminUser {
Username: u.Username, Username: u.Username,
DisplayName: u.DisplayName, DisplayName: u.DisplayName,
Role: gen.Role(u.Role), Role: gen.Role(u.Role),
ReviewsUploads: u.ReviewsUploads,
Locale: u.Locale, Locale: u.Locale,
IsBlocked: u.IsBlocked, IsBlocked: u.IsBlocked,
MustChangePassword: u.MustChangePassword, MustChangePassword: u.MustChangePassword,
+25
View File
@@ -395,3 +395,28 @@ func TestAudit(t *testing.T) {
mod := e.signIn("mod", "moderator-pass-1") mod := e.signIn("mod", "moderator-pass-1")
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodGet, "/api/v1/admin/audit", nil, bearer(mod)).code()) 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"])
}
@@ -303,6 +303,9 @@ func (s *MemStore) UpdateUser(ctx context.Context, id uuid.UUID, upd auth.UserUp
if upd.IsBlocked != nil { if upd.IsBlocked != nil {
u.IsBlocked = *upd.IsBlocked u.IsBlocked = *upd.IsBlocked
} }
if upd.ReviewsUploads != nil {
u.ReviewsUploads = *upd.ReviewsUploads
}
if upd.RevokeTokens { if upd.RevokeTokens {
u.TokenVersion++ u.TokenVersion++
} }
+1
View File
@@ -231,6 +231,7 @@ func MeDTO(u User) gen.Me {
Username: u.Username, Username: u.Username,
DisplayName: u.DisplayName, DisplayName: u.DisplayName,
Role: gen.Role(u.Role), Role: gen.Role(u.Role),
ReviewsUploads: u.ReviewsUploads,
Locale: u.Locale, Locale: u.Locale,
UiPrefs: uiPrefsDTO(u.UIPrefs), UiPrefs: uiPrefsDTO(u.UIPrefs),
MustChangePassword: u.MustChangePassword, MustChangePassword: u.MustChangePassword,
+2
View File
@@ -54,6 +54,7 @@ type User struct {
DisplayName *string DisplayName *string
PasswordHash string PasswordHash string
Role Role Role Role
ReviewsUploads bool // аудит очереди на добавление; не следует из роли
Locale string Locale string
UIPrefs UIPrefs UIPrefs UIPrefs
MustChangePassword bool MustChangePassword bool
@@ -94,6 +95,7 @@ type Session struct {
type Principal struct { type Principal struct {
UserID uuid.UUID UserID uuid.UUID
Role Role Role Role
ReviewsUploads bool
SessionID uuid.UUID SessionID uuid.UUID
MustChangePassword bool MustChangePassword bool
MustChangeCredentials bool MustChangeCredentials bool
+8 -6
View File
@@ -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, 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) { func scanUser(row pgx.Row) (User, error) {
var ( 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, 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.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) { if errors.Is(err, pgx.ErrNoRows) {
return User{}, ErrNotFound return User{}, ErrNotFound
} }
@@ -81,10 +82,10 @@ func (s *PgStore) CreateUser(ctx context.Context, u User, createdBy *uuid.UUID)
} }
_, err = s.q.Exec(ctx, ` _, err = s.q.Exec(ctx, `
INSERT INTO users (id, username, display_name, password_hash, role, locale, ui_prefs, 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) 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)`, 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.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) { if isUniqueViolation(err) {
return ErrUsernameTaken 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, display_name = CASE WHEN $2 THEN $3 ELSE display_name END,
role = COALESCE($4::user_role, role), role = COALESCE($4::user_role, role),
is_blocked = COALESCE($5, is_blocked), is_blocked = COALESCE($5, is_blocked),
reviews_uploads = COALESCE($7, reviews_uploads),
token_version = token_version + CASE WHEN $6 THEN 1 ELSE 0 END, token_version = token_version + CASE WHEN $6 THEN 1 ELSE 0 END,
updated_at = now() updated_at = now()
WHERE id = $1 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. // ResetPassword реализует Store.
+10 -2
View File
@@ -14,7 +14,7 @@ const (
PermManageLibrary // хранилища, сканирование, переиндексация PermManageLibrary // хранилища, сканирование, переиндексация
PermViewLibraryStatus // статус библиотеки (просмотр) PermViewLibraryStatus // статус библиотеки (просмотр)
PermSuggestTracks // предлагать треки, видеть свои предложения PermSuggestTracks // предлагать треки, видеть свои предложения
PermReviewUploads // очередь, одобрение, правка метаданных, лимиты PermReviewUploads // очередь, одобрение, правка метаданных; даёт флаг, не роль
PermSystemStatus // статус рекомендательной системы, аудит PermSystemStatus // статус рекомендательной системы, аудит
) )
@@ -24,11 +24,11 @@ var permissions = map[Permission][]Role{
PermManageLibrary: {RoleAdmin}, PermManageLibrary: {RoleAdmin},
PermViewLibraryStatus: {RoleModerator, RoleAdmin}, PermViewLibraryStatus: {RoleModerator, RoleAdmin},
PermSuggestTracks: {RoleModerator, RoleAdmin}, PermSuggestTracks: {RoleModerator, RoleAdmin},
PermReviewUploads: {RoleAdmin},
PermSystemStatus: {RoleAdmin}, PermSystemStatus: {RoleAdmin},
} }
// Can сообщает, есть ли у роли глобальное право. // Can сообщает, есть ли у роли глобальное право.
// Аудит загрузок сюда не входит: его проверяет Principal.Allowed.
func Can(r Role, p Permission) bool { func Can(r Role, p Permission) bool {
for _, allowed := range permissions[p] { for _, allowed := range permissions[p] {
if allowed == r { if allowed == r {
@@ -38,6 +38,14 @@ func Can(r Role, p Permission) bool {
return false return false
} }
// Allowed — право конкретного пользователя. Аудит загрузок не вытекает из роли admin.
func (p Principal) Allowed(perm Permission) bool {
if perm == PermReviewUploads {
return p.ReviewsUploads
}
return Can(p.Role, perm)
}
// UserAction — действие над учётной записью другого пользователя. // UserAction — действие над учётной записью другого пользователя.
type UserAction int type UserAction int
+8 -1
View File
@@ -20,7 +20,6 @@ func TestCan(t *testing.T) {
{PermManageLibrary, "manage library", [3]bool{false, false, true}}, {PermManageLibrary, "manage library", [3]bool{false, false, true}},
{PermViewLibraryStatus, "library status", [3]bool{false, true, true}}, {PermViewLibraryStatus, "library status", [3]bool{false, true, true}},
{PermSuggestTracks, "suggest tracks", [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}}, {PermSystemStatus, "system status", [3]bool{false, false, true}},
} }
roles := [3]Role{RoleUser, RoleModerator, RoleAdmin} roles := [3]Role{RoleUser, RoleModerator, RoleAdmin}
@@ -72,6 +71,14 @@ func TestCanManageUserSelf(t *testing.T) {
assert.True(t, CanManageUser(admin, admin, UserResetPassword)) 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) { func TestCanAssignRole(t *testing.T) {
cases := []struct { cases := []struct {
actor, role Role actor, role Role
+2 -1
View File
@@ -368,6 +368,7 @@ func (s *Service) Authenticate(ctx context.Context, access string) (Principal, e
return Principal{ return Principal{
UserID: u.ID, UserID: u.ID,
Role: u.Role, Role: u.Role,
ReviewsUploads: u.ReviewsUploads,
SessionID: claims.SessionID, SessionID: claims.SessionID,
MustChangePassword: u.MustChangePassword, MustChangePassword: u.MustChangePassword,
MustChangeCredentials: u.MustChangeCredentials, MustChangeCredentials: u.MustChangeCredentials,
@@ -621,7 +622,7 @@ func (s *Service) Bootstrap(ctx context.Context) error {
} }
u := User{ u := User{
ID: s.ids.New(), Username: username, PasswordHash: hash, Role: RoleAdmin, Locale: "ru", 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 { err = s.store.WithTx(ctx, func(tx Store) error {
if err := tx.CreateUser(ctx, u, nil); err != nil { if err := tx.CreateUser(ctx, u, nil); err != nil {
+5 -4
View File
@@ -31,10 +31,11 @@ type UserFilter struct {
// UserUpdate — изменения из админки. nil — не менять. // UserUpdate — изменения из админки. nil — не менять.
type UserUpdate struct { type UserUpdate struct {
DisplayName **string DisplayName **string
Role *Role Role *Role
IsBlocked *bool IsBlocked *bool
RevokeTokens bool ReviewsUploads *bool
RevokeTokens bool
} }
// Store — доступ к users, refresh_tokens, audit_log и app_settings. // Store — доступ к users, refresh_tokens, audit_log и app_settings.
+1 -1
View File
@@ -53,7 +53,7 @@ func (h *Handler) fileDTO(ctx context.Context, f File) gen.UploadFile {
out.TusUrl = &u out.TusUrl = &u
} }
if h.signer != nil && f.StagingPath != "" && f.Status.KeepsStaging() { 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) u := h.streamURL(f.ID)
out.StreamUrl = &u out.StreamUrl = &u
} }
+4 -4
View File
@@ -67,7 +67,7 @@ func principal(ctx context.Context, perm auth.Permission) (auth.Principal, error
if !ok { if !ok {
return auth.Principal{}, apperr.Unauthorized(auth.CodeUnauthorized, "not authenticated") 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 auth.Principal{}, apperr.Forbidden(auth.CodeForbidden, "not allowed")
} }
return p, nil 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) return s.store.File(ctx, id)
} }
// GetSettings — настройки очереди (админ). // GetSettings — лимиты очереди (админ, не аудитор загрузок).
func (s *Service) GetSettings(ctx context.Context) (Settings, error) { 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 Settings{}, err
} }
return s.store.Settings(ctx) return s.store.Settings(ctx)
@@ -446,7 +446,7 @@ type SettingsInput struct {
// UpdateSettings меняет лимиты; уменьшение ниже занятого ничего не удаляет (docs/uploads.md#3). // UpdateSettings меняет лимиты; уменьшение ниже занятого ничего не удаляет (docs/uploads.md#3).
func (s *Service) UpdateSettings(ctx context.Context, in SettingsInput) (Settings, error) { 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 { if err != nil {
return Settings{}, err return Settings{}, err
} }
@@ -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;