backup engine fixes: shared-repo unlock, non-root uid reads, lock release, local-path repo, backblaze key id

This commit is contained in:
jarek committed 2026-07-31 10:10:55 +02:00
1 parent e700b5b6aa
commit e3c07f32da
15 files changed
+375 -313

No files matched your search

+3 -29
View File
@@ -4,26 +4,12 @@ import { authorize } from '$lib/server/authorize';
import { auditBackup } from '$lib/server/audit';
import {
getBackupConfigs,
createBackupConfig,
getBackupDestination,
getEnvironment
createBackupConfig
} from '$lib/server/db';
import { registerSchedule, isValidCron } from '$lib/server/scheduler';
import { assertStackBackupable } from '$lib/server/backups';
import { validateRetention, retentionToStore } from '$lib/server/backups/helpers';
// Inlined to avoid pulling lucide-svelte / svelte runtime into the server bundle
// via $lib/utils/backup. Keep in sync with that module if the predicates change.
function isLocalRepo(repository: string): boolean {
return repository.startsWith('/') || repository.startsWith('./');
}
function isRemoteEnv(connectionType?: string | null, host?: string | null): boolean {
if (!connectionType) return false;
if (connectionType === 'hawser-standard' || connectionType === 'hawser-edge') return true;
if (connectionType === 'direct' && !!host) return true;
return false;
}
export const GET: RequestHandler = async ({ url, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('backups', 'view')) {
@@ -92,20 +78,8 @@ export const POST: RequestHandler = async (event) => {
return json({ error: 'Access denied to this environment' }, { status: 403 });
}
// Refuse local repo + remote env: the restic helper runs on the REMOTE
// daemon, which has no access to Dockhand's filesystem. The backup would
// silently write to a wrong path on the remote host.
if (body.environmentId) {
const [dest, env] = await Promise.all([
getBackupDestination(body.destinationId),
getEnvironment(body.environmentId)
]);
if (dest && env && isRemoteEnv(env.connectionType, env.host) && isLocalRepo(dest.repository)) {
return json({
error: `Local repository "${dest.name}" cannot back up containers on remote environment "${env.name}". Use S3, REST, or another non-local backend.`
}, { status: 400 });
}
}
// A local-path repo is allowed on any env; if the target daemon can't see the
// repo (wrong-host mount) the backup fails loud via the helper's localRepoGuard.
// External stacks (no known compose path) can't be backed up usefully —
// see assertStackBackupable for the why.
+3 -19
View File
@@ -4,15 +4,12 @@ import { authorize } from '$lib/server/authorize';
import { auditBackup } from '$lib/server/audit';
import {
updateBackupConfig,
deleteBackupConfig,
getBackupDestination,
getEnvironment
deleteBackupConfig
} from '$lib/server/db';
import { registerSchedule, unregisterSchedule, isValidCron } from '$lib/server/scheduler';
import { isBackupRunning } from '$lib/server/backups';
import { validateRetention, retentionToStore, resolveEnabledOnScheduleChange } from '$lib/server/backups/helpers';
import { requireBackups, loadConfigGateEnv } from '$lib/server/backups/route-guards';
import { isLocalRepo, isRemoteEnvironment } from '$lib/shared/repo-predicates';
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
@@ -59,21 +56,8 @@ export const PUT: RequestHandler = async (event) => {
return json({ error: 'Cannot change the destination while a backup is running for this config' }, { status: 409 });
}
// Refuse local repo + remote env (same rule as POST). Effective destination
// is the body's value if supplied, otherwise the existing config's.
const effectiveDestId = body.destinationId ?? existing.destinationId;
const effectiveEnvId = existing.environmentId; // env is fixed at creation
if (effectiveEnvId) {
const [dest, env] = await Promise.all([
getBackupDestination(effectiveDestId),
getEnvironment(effectiveEnvId)
]);
if (dest && env && isRemoteEnvironment(env) && isLocalRepo(dest.repository)) {
return json({
error: `Local repository "${dest.name}" cannot back up containers on remote environment "${env.name}". Use S3, REST, or another non-local backend.`
}, { status: 400 });
}
}
// A local-path repo is allowed on any env; a wrong-host mount fails loud via
// the helper's localRepoGuard at run time.
// Auto-enable a config that transitions from manual (no schedule) to scheduled
// (a real cron) — otherwise a paused run-once config that the user edits to add a
@@ -7,25 +7,12 @@ import {
updateBackupDestination,
deleteBackupDestination,
decryptBackupDestination,
getBackupConfigs,
getEnvironment
getBackupConfigs
} from '$lib/server/db';
import { registerSchedule, unregisterSchedule } from '$lib/server/scheduler';
import { validatePolicySchedules, validateRepositoryForSave, validateFlags } from '$lib/server/backups/helpers';
import { destinationHasRunningBackup } from '$lib/server/backups';
// Inlined predicates (kept in sync with the config routes) — see
// configs/+server.ts. A local repo cannot back up a remote environment.
function isLocalRepo(repository: string): boolean {
return repository.startsWith('/') || repository.startsWith('./');
}
function isRemoteEnv(connectionType?: string | null, host?: string | null): boolean {
if (!connectionType) return false;
if (connectionType === 'hawser-standard' || connectionType === 'hawser-edge') return true;
if (connectionType === 'direct' && !!host) return true;
return false;
}
/**
* Single-destination response shape. envVars (decrypted cloud credentials) are
* ONLY included for callers with backups:manage — the edit form that pre-fills
@@ -98,23 +85,8 @@ export const PUT: RequestHandler = async (event) => {
const flagError = validateFlags(body.flags);
if (flagError) return json({ error: flagError }, { status: 400 });
// (audit #8) Changing the repository to a LOCAL path bypasses the config-route
// guard: refuse if any config on this destination targets a remote environment
// (the restic helper runs on the remote daemon, which can't see Dockhand's fs).
const effectiveRepository = body.repository !== undefined ? body.repository : existing.repository;
if (body.repository !== undefined && isLocalRepo(effectiveRepository)) {
const configs = await getBackupConfigs();
const envIds = [...new Set(
configs.filter((c: any) => c.destinationId === id && c.environmentId).map((c: any) => c.environmentId as number)
)];
const envs = await Promise.all(envIds.map((eid) => getEnvironment(eid)));
const remote = envs.find((env) => env && isRemoteEnv(env.connectionType, env.host));
if (remote) {
return json({
error: `Local repository cannot back up containers on remote environment "${remote.name}". Use S3, REST, or another non-local backend.`
}, { status: 400 });
}
}
// A local-path repo is allowed on any env; a wrong-host mount fails loud via
// the helper's localRepoGuard at backup/restore time.
try {
const updated = await updateBackupDestination(id, {