fix(api): пускать к бэкапу и миграции только глобального админа

Скачивание и восстановление файла БД, JSON-бэкап и legacy-импорт
затрагивают все spaces и содержат учётные данные API, поэтому прав
vps:settings:admin в одном space для них недостаточно.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-06 22:36:53 +07:00
1 parent 273883f657
commit 0b9b1cad54
3 files changed
+21

No files matched your search

+14
View File
@@ -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')
+5
View File
@@ -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 },
+2
View File
@@ -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' } })