backup engine fixes: shared-repo unlock, non-root uid reads, lock release, local-path repo, backblaze key id
This commit is contained in:
15 files changed
+375
-313
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in new issue
Block a user