From 0b9b1cad540a06608817cad24782f9ba6162141a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 22:36:53 +0700 Subject: [PATCH] =?UTF-8?q?fix(api):=20=D0=BF=D1=83=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BA=20=D0=B1=D1=8D=D0=BA=D0=B0=D0=BF=D1=83=20?= =?UTF-8?q?=D0=B8=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B3=D0=BB=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B0=D0=B4=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скачивание и восстановление файла БД, JSON-бэкап и legacy-импорт затрагивают все spaces и содержат учётные данные API, поэтому прав vps:settings:admin в одном space для них недостаточно. Co-Authored-By: Claude Opus 5.5 --- apps/api/src/plugins/space.ts | 14 ++++++++++++++ apps/api/src/routes/backup.ts | 5 +++++ apps/api/src/routes/migrate.ts | 2 ++ 3 files changed, 21 insertions(+) diff --git a/apps/api/src/plugins/space.ts b/apps/api/src/plugins/space.ts index ee50796..956ab82 100644 --- a/apps/api/src/plugins/space.ts +++ b/apps/api/src/plugins/space.ts @@ -186,6 +186,20 @@ export function requireSpaceRole( return true } +/** + * Whole-DB operations (backup download/restore, legacy import) cross every space and carry API credentials, + * so per-space settings admin is not enough. + */ +export function requireGlobalAdmin(request: FastifyRequest, reply: FastifyReply): boolean { + const user = request.authUser + if (!user) return true + if (user.isAdmin || hasPermission(user.permissions, 'vps:spaces:admin')) return true + void reply.code(403).send({ + error: { code: 'FORBIDDEN', message: 'Операция доступна только глобальному администратору' }, + }) + return false +} + export function canWriteInSpace(request: FastifyRequest): boolean { const role = request.spaceRole ?? 'viewer' return roleAtLeast(role, 'member') diff --git a/apps/api/src/routes/backup.ts b/apps/api/src/routes/backup.ts index 1a72188..b1e2647 100644 --- a/apps/api/src/routes/backup.ts +++ b/apps/api/src/routes/backup.ts @@ -15,6 +15,7 @@ import { type FullBackupPayload, } from '../services/backup-full.js' import { restartScheduler } from '../services/scheduler.js' +import { requireGlobalAdmin } from '../plugins/space.js' /** Лимит тела для импорта бэкапа (Fastify default = 1 MiB → 413). */ function backupBodyLimitBytes(): number { @@ -29,6 +30,10 @@ function backupBodyLimitBytes(): number { export const backupRoutes: FastifyPluginAsync = async (app) => { const bodyLimit = backupBodyLimitBytes() + app.addHook('preHandler', async (req, reply) => { + if (!requireGlobalAdmin(req, reply)) return reply + }) + app.addContentTypeParser( 'application/octet-stream', { parseAs: 'buffer', bodyLimit }, diff --git a/apps/api/src/routes/migrate.ts b/apps/api/src/routes/migrate.ts index 78ef827..16cdafc 100644 --- a/apps/api/src/routes/migrate.ts +++ b/apps/api/src/routes/migrate.ts @@ -1,8 +1,10 @@ import type { FastifyPluginAsync } from 'fastify' import { consolidateAllProviderApiSources, getSqlite } from '@cfdm/db' +import { requireGlobalAdmin } from '../plugins/space.js' export const migrateRoutes: FastifyPluginAsync = async (app) => { app.post('/api/migrate', async (req, reply) => { + if (!requireGlobalAdmin(req, reply)) return reply const data = req.body if (!data || typeof data !== 'object') { return reply.code(400).send({ error: { code: 'VALIDATION', message: 'Invalid payload' } })