mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-04 22:40:11 +00:00
fix(backup): redact destination secrets in settings
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
loadBackupSettings,
|
loadBackupSettings,
|
||||||
normalizeBackupSettingsInput,
|
normalizeBackupSettingsInput,
|
||||||
normalizeImportedBackupSettings,
|
normalizeImportedBackupSettings,
|
||||||
|
redactBackupSettingsSecrets,
|
||||||
repairBackupSettings,
|
repairBackupSettings,
|
||||||
requireBackupDestination,
|
requireBackupDestination,
|
||||||
saveBackupSettings,
|
saveBackupSettings,
|
||||||
@@ -675,6 +676,7 @@ function collectExternalRemoteAttachmentBlobNames(archiveBytes: Uint8Array): str
|
|||||||
function toImportStatusCode(message: string): number {
|
function toImportStatusCode(message: string): number {
|
||||||
const lower = message.toLowerCase();
|
const lower = message.toLowerCase();
|
||||||
if (lower.includes('checksum')) return 400;
|
if (lower.includes('checksum')) return 400;
|
||||||
|
if (lower.includes('invalid remote backup path') || lower.includes('please select a backup zip file')) return 409;
|
||||||
if (lower.includes('invalid backup') || lower.includes('invalid json')) return 400;
|
if (lower.includes('invalid backup') || lower.includes('invalid json')) return 400;
|
||||||
if (lower.includes('fresh instance')) return 409;
|
if (lower.includes('fresh instance')) return 409;
|
||||||
if (lower.includes('not configured') || lower.includes('kv')) return 409;
|
if (lower.includes('not configured') || lower.includes('kv')) return 409;
|
||||||
@@ -858,7 +860,7 @@ export async function handleGetAdminBackupSettings(request: Request, env: Env, a
|
|||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
try {
|
try {
|
||||||
const settings = await loadBackupSettings(storage, env, 'UTC');
|
const settings = await loadBackupSettings(storage, env, 'UTC');
|
||||||
return jsonResponse(settings);
|
return jsonResponse(redactBackupSettingsSecrets(settings));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error instanceof Error ? error.message : 'Backup settings could not be loaded', 409);
|
return errorResponse(error instanceof Error ? error.message : 'Backup settings could not be loaded', 409);
|
||||||
}
|
}
|
||||||
@@ -897,7 +899,7 @@ export async function handleUpdateAdminBackupSettings(request: Request, env: Env
|
|||||||
destinationCount: next.destinations.length,
|
destinationCount: next.destinations.length,
|
||||||
scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length,
|
scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length,
|
||||||
}, request);
|
}, request);
|
||||||
return jsonResponse(next);
|
return jsonResponse(redactBackupSettingsSecrets(next));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleGetAdminBackupSettingsRepairState(request: Request, env: Env, actorUser: User): Promise<Response> {
|
export async function handleGetAdminBackupSettingsRepairState(request: Request, env: Env, actorUser: User): Promise<Response> {
|
||||||
@@ -950,7 +952,7 @@ export async function handleRepairAdminBackupSettings(request: Request, env: Env
|
|||||||
destinationCount: next.destinations.length,
|
destinationCount: next.destinations.length,
|
||||||
scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length,
|
scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length,
|
||||||
}, request);
|
}, request);
|
||||||
return jsonResponse(next);
|
return jsonResponse(redactBackupSettingsSecrets(next));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleRunAdminConfiguredBackup(request: Request, env: Env, actorUser: User): Promise<Response> {
|
export async function handleRunAdminConfiguredBackup(request: Request, env: Env, actorUser: User): Promise<Response> {
|
||||||
@@ -987,7 +989,7 @@ export async function handleRunAdminConfiguredBackup(request: Request, env: Env,
|
|||||||
provider: outcome.result.provider,
|
provider: outcome.result.provider,
|
||||||
remotePath: outcome.result.remotePath,
|
remotePath: outcome.result.remotePath,
|
||||||
},
|
},
|
||||||
settings: outcome.settings,
|
settings: redactBackupSettingsSecrets(outcome.settings),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error instanceof Error ? error.message : 'Backup run failed', 500);
|
return errorResponse(error instanceof Error ? error.message : 'Backup run failed', 500);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
export const BACKUP_SETTINGS_CONFIG_KEY = 'backup.settings.v1';
|
export const BACKUP_SETTINGS_CONFIG_KEY = 'backup.settings.v1';
|
||||||
const BACKUP_RUNTIME_CONFIG_KEY = 'backup.runtime.v1';
|
const BACKUP_RUNTIME_CONFIG_KEY = 'backup.runtime.v1';
|
||||||
export const BACKUP_SCHEDULER_WINDOW_MINUTES = 5;
|
export const BACKUP_SCHEDULER_WINDOW_MINUTES = 5;
|
||||||
|
export const REDACTED_BACKUP_SECRET = '********';
|
||||||
const MAX_BACKUP_DESTINATIONS = 24;
|
const MAX_BACKUP_DESTINATIONS = 24;
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
@@ -180,6 +181,32 @@ function normalizeDestination(
|
|||||||
return normalizeWebDavDestination(destination, allowIncomplete);
|
return normalizeWebDavDestination(destination, allowIncomplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldPreserveBackupSecret(value: unknown): boolean {
|
||||||
|
if (value === undefined || value === null) return true;
|
||||||
|
const raw = String(value);
|
||||||
|
return raw === '' || raw === REDACTED_BACKUP_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withPreservedDestinationSecret(
|
||||||
|
destinationType: BackupDestinationType,
|
||||||
|
inputDestination: unknown,
|
||||||
|
previous: BackupDestinationRecord | undefined
|
||||||
|
): unknown {
|
||||||
|
const source = isPlainObject(inputDestination) ? { ...inputDestination } : {};
|
||||||
|
if (destinationType === 's3') {
|
||||||
|
const previousDestination = previous?.type === 's3' ? previous.destination as S3BackupDestination : null;
|
||||||
|
if (shouldPreserveBackupSecret(source.secretAccessKey)) {
|
||||||
|
source.secretAccessKey = previousDestination?.secretAccessKey || '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const previousDestination = previous?.type === 'webdav' ? previous.destination as WebDavBackupDestination : null;
|
||||||
|
if (shouldPreserveBackupSecret(source.password)) {
|
||||||
|
source.password = previousDestination?.password || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeRuntime(value: unknown): BackupRuntimeState {
|
function normalizeRuntime(value: unknown): BackupRuntimeState {
|
||||||
const source = isPlainObject(value) ? value : {};
|
const source = isPlainObject(value) ? value : {};
|
||||||
const asIso = (input: unknown): string | null => {
|
const asIso = (input: unknown): string | null => {
|
||||||
@@ -250,7 +277,11 @@ function normalizeDestinationRecord(
|
|||||||
retentionCount: normalizeRetentionCount(retentionSource, previousSchedule.retentionCount),
|
retentionCount: normalizeRetentionCount(retentionSource, previousSchedule.retentionCount),
|
||||||
};
|
};
|
||||||
|
|
||||||
const destination = normalizeDestination(type, input.destination, !schedule.enabled);
|
const destination = normalizeDestination(
|
||||||
|
type,
|
||||||
|
withPreservedDestinationSecret(type, input.destination, previous),
|
||||||
|
!schedule.enabled
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -432,6 +463,31 @@ export function serializeBackupSettings(settings: BackupSettings): string {
|
|||||||
return JSON.stringify(stripRuntimeFromSettings(settings));
|
return JSON.stringify(stripRuntimeFromSettings(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function redactBackupSettingsSecrets(settings: BackupSettings): BackupSettings {
|
||||||
|
return {
|
||||||
|
destinations: settings.destinations.map((destination) => {
|
||||||
|
if (destination.type === 's3') {
|
||||||
|
const config = destination.destination as S3BackupDestination;
|
||||||
|
return {
|
||||||
|
...destination,
|
||||||
|
destination: {
|
||||||
|
...config,
|
||||||
|
secretAccessKey: config.secretAccessKey ? REDACTED_BACKUP_SECRET : '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const config = destination.destination as WebDavBackupDestination;
|
||||||
|
return {
|
||||||
|
...destination,
|
||||||
|
destination: {
|
||||||
|
...config,
|
||||||
|
password: config.password ? REDACTED_BACKUP_SECRET : '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadBackupSettings(storage: StorageService, env: Env, fallbackTimezone: string = 'UTC'): Promise<BackupSettings> {
|
export async function loadBackupSettings(storage: StorageService, env: Env, fallbackTimezone: string = 'UTC'): Promise<BackupSettings> {
|
||||||
const raw = await storage.getConfigValue(BACKUP_SETTINGS_CONFIG_KEY);
|
const raw = await storage.getConfigValue(BACKUP_SETTINGS_CONFIG_KEY);
|
||||||
const mergeRuntime = async (settings: BackupSettings): Promise<BackupSettings> => (
|
const mergeRuntime = async (settings: BackupSettings): Promise<BackupSettings> => (
|
||||||
|
|||||||
Reference in New Issue
Block a user