backup engine internals - streaming, timeouts, typed snapshot layout, volume keys, route guards
This commit is contained in:
32 files changed
+1205
-352
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { auditBackup } from '$lib/server/audit';
|
||||
import {
|
||||
getBackupConfigs,
|
||||
@@ -12,9 +13,8 @@ import { validateRetention, retentionToStore } from '$lib/server/backups/helpers
|
||||
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'view')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'view');
|
||||
if (denied) return denied;
|
||||
|
||||
const type = url.searchParams.get('type') ?? undefined;
|
||||
const targetName = url.searchParams.get('target') ?? undefined;
|
||||
@@ -45,9 +45,8 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { request, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'manage');
|
||||
if (denied) return denied;
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { auditBackupDestination } from '$lib/server/audit';
|
||||
import { getBackupDestination, updateBackupDestinationTestStatus } from '$lib/server/db';
|
||||
import { initRepository } from '$lib/server/backups';
|
||||
@@ -8,9 +9,8 @@ import { initRepository } from '$lib/server/backups';
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'manage');
|
||||
if (denied) return denied;
|
||||
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id)) return json({ error: 'Invalid ID' }, { status: 400 });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { auditBackupDestination } from '$lib/server/audit';
|
||||
import { getBackupDestination } from '$lib/server/db';
|
||||
import { rotateDestinationPassword } from '$lib/server/backups';
|
||||
@@ -20,9 +21,8 @@ import { rotateDestinationPassword } from '$lib/server/backups';
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, request, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'manage');
|
||||
if (denied) return denied;
|
||||
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id)) return json({ error: 'Invalid ID' }, { status: 400 });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import {
|
||||
getBackupDestination,
|
||||
updateBackupDestinationTestStatus
|
||||
@@ -9,9 +10,8 @@ import { testRepository } from '$lib/server/backups';
|
||||
|
||||
export const POST: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'manage');
|
||||
if (denied) return denied;
|
||||
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id)) return json({ error: 'Invalid ID' }, { status: 400 });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { auditBackupDestination } from '$lib/server/audit';
|
||||
import { getBackupDestination } from '$lib/server/db';
|
||||
import { verifyBackup } from '$lib/server/backups';
|
||||
@@ -9,9 +10,8 @@ import { createJobResponse } from '$lib/server/sse';
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { params, request, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'manage');
|
||||
if (denied) return denied;
|
||||
|
||||
const destinationId = parseInt(params.id);
|
||||
if (isNaN(destinationId)) return json({ error: 'Invalid destination ID' }, { status: 400 });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { auditRestore } from '$lib/server/audit';
|
||||
import { cancelRestore } from '$lib/server/backups';
|
||||
import { isValidSnapshotId } from '$lib/server/backups/helpers';
|
||||
@@ -14,9 +15,8 @@ import { isValidSnapshotId } from '$lib/server/backups/helpers';
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const { request, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'manage');
|
||||
if (denied) return denied;
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const snapshotId: string | undefined = body.snapshotId;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
|
||||
import { validateSnapshotId } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { auditBackup } from '$lib/server/audit';
|
||||
import { getBackupDestination } from '$lib/server/db';
|
||||
import { forgetSnapshot } from '$lib/server/backups';
|
||||
@@ -10,9 +11,8 @@ import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
const { params, url, cookies } = event;
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const rbacDenied = await requireBackups(auth, 'manage');
|
||||
if (rbacDenied) return rbacDenied;
|
||||
|
||||
const snapshotId = params.id;
|
||||
const invalidSnap = validateSnapshotId(snapshotId);
|
||||
|
||||
@@ -2,14 +2,14 @@ import { json } from '@sveltejs/kit';
|
||||
import { validateSnapshotId } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { browseSnapshot } from '$lib/server/backups';
|
||||
import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'view')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'view');
|
||||
if (denied) return denied;
|
||||
|
||||
const snapshotId = params.id;
|
||||
const invalidSnap = validateSnapshotId(snapshotId);
|
||||
|
||||
@@ -2,14 +2,15 @@ import { json } from '@sveltejs/kit';
|
||||
import { validateSnapshotId } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { dumpSnapshotFile, dumpSnapshotFileBytes, dumpSnapshotArchive } from '$lib/server/backups';
|
||||
import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
|
||||
import { parseSnapshotLayout, redactSnapshotLayout } from '$lib/server/backups/snapshot-layout';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'view')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'view');
|
||||
if (denied) return denied;
|
||||
|
||||
const snapshotId = params.id;
|
||||
const invalidSnap = validateSnapshotId(snapshotId);
|
||||
@@ -48,6 +49,26 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
return json({ error: 'Invalid path' }, { status: 400 });
|
||||
}
|
||||
|
||||
// metadata.json carries secrets (stack.secrets ciphertext + container Config.Env
|
||||
// plaintext). It must ONLY leave the process through the redacting path — never as a
|
||||
// raw file/byte/archive dump, which would bypass redaction. Serve a redacted inline
|
||||
// preview; refuse raw downloads and reject a /metadata archive dump that would embed it.
|
||||
const isMetadataFile = path === '/metadata/metadata.json';
|
||||
if (isMetadataFile || (isDir && download && (path === '/metadata' || path === '/metadata/'))) {
|
||||
if (download) {
|
||||
return json({ error: 'metadata.json cannot be downloaded raw; use the snapshot metadata endpoint (secrets are redacted there)' }, { status: 403 });
|
||||
}
|
||||
try {
|
||||
const raw = await dumpSnapshotFile(destinationId, snapshotId, '/metadata/metadata.json');
|
||||
const layout = parseSnapshotLayout(raw);
|
||||
if (!layout) return json({ error: 'metadata unreadable' }, { status: 404 });
|
||||
return json({ content: JSON.stringify(redactSnapshotLayout(layout), null, 2) });
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
return json({ error: errorMsg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize filename for Content-Disposition (strip quotes, backslashes, control chars)
|
||||
const sanitizeFilename = (name: string) => name.replace(/["\\\x00-\x1f]/g, '_');
|
||||
|
||||
|
||||
@@ -2,14 +2,15 @@ import { json } from '@sveltejs/kit';
|
||||
import { validateSnapshotId } from '$lib/server/docker-validation';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { getSnapshotMetadata } from '$lib/server/backups';
|
||||
import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
|
||||
import { redactSnapshotLayout } from '$lib/server/backups/snapshot-layout';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'view')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'view');
|
||||
if (denied) return denied;
|
||||
|
||||
const snapshotId = params.id;
|
||||
const invalidSnap = validateSnapshotId(snapshotId);
|
||||
@@ -28,15 +29,9 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
try {
|
||||
const metadata = await getSnapshotMetadata(destinationId, snapshotId);
|
||||
if (!metadata) return json({ error: 'No metadata available' }, { status: 404 });
|
||||
// Never expose the stored secret VALUES (ciphertext) to the client — replace the
|
||||
// `secrets` array with `secretKeys` (names only) so the UI can list them.
|
||||
const { secrets, ...safe } = metadata as Record<string, unknown>;
|
||||
if (Array.isArray(secrets)) {
|
||||
(safe as Record<string, unknown>).secretKeys = secrets
|
||||
.map((s: any) => s?.key)
|
||||
.filter((k: unknown): k is string => typeof k === 'string');
|
||||
}
|
||||
return json(safe);
|
||||
// Single redaction point: strips stack.secrets values AND container inspect
|
||||
// Config.Env/Labels (plaintext secrets) before anything reaches the client.
|
||||
return json(redactSnapshotLayout(metadata));
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
return json({ error: msg }, { status: 500 });
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { requireBackups } from '$lib/server/backups/route-guards';
|
||||
import { diffSnapshots } from '$lib/server/backups';
|
||||
import { guardSnapshotEnvAccess } from '$lib/server/backups/route-guards';
|
||||
import { validateSnapshotId } from '$lib/server/docker-validation';
|
||||
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('backups', 'view')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
const denied = await requireBackups(auth, 'view');
|
||||
if (denied) return denied;
|
||||
|
||||
const destId = url.searchParams.get('destinationId');
|
||||
const snapA = url.searchParams.get('snapshotA');
|
||||
|
||||
Reference in new issue
Block a user