fix: conditionally include secrets in user data backup (#5360) * fix: conditionally include secrets in user data backup * feat: add full data backup toggle * 418 -> 403 I'm not a teapot * Distinguish fails from disabled
Signed| @@ -165,6 +165,8 @@ rateLimiting: | |||
| 165 | 165 | ||
| 166 | ## BACKUP CONFIGURATION | 166 | ## BACKUP CONFIGURATION |
| 167 | backups: | 167 | backups: |
| 168 | # Allow users to create a full backup archive of their data | ||
| 169 | allowFullDataBackup: true | ||
| 168 | # Common settings for all backup types | 170 | # Common settings for all backup types |
| 169 | common: | 171 | common: |
| 170 | # Number of backups to keep for each chat and settings file | 172 | # Number of backups to keep for each chat and settings file |
| @@ -40,7 +40,7 @@ declare global { | |||
| 40 | /** | 40 | /** |
| 41 | * Authenticated user handle. | 41 | * Authenticated user handle. |
| 42 | */ | 42 | */ |
| 43 | handle: string; | 43 | handle: string | null; |
| 44 | /** | 44 | /** |
| 45 | * Last time the session was extended. | 45 | * Last time the session was extended. |
| 46 | */ | 46 | */ |
| @@ -277,6 +277,29 @@ function getActiveSecretLabel(key) { | |||
| 277 | return ''; | 277 | return ''; |
| 278 | } | 278 | } |
| 279 | 279 | ||
| 280 | /** | ||
| 281 | * Checks if secrets can be viewed based on server configuration. | ||
| 282 | * @returns {Promise<boolean|null>} A boolean value, or null if the request fails. | ||
| 283 | */ | ||
| 284 | export async function canViewSecrets() { | ||
| 285 | try { | ||
| 286 | const response = await fetch('/api/secrets/settings', { | ||
| 287 | method: 'POST', | ||
| 288 | headers: getRequestHeaders({ omitContentType: true }), | ||
| 289 | }); | ||
| 290 | |||
| 291 | if (!response.ok) { | ||
| 292 | return null; | ||
| 293 | } | ||
| 294 | |||
| 295 | const data = await response.json(); | ||
| 296 | return data?.allowKeysExposure === true; | ||
| 297 | } catch (error) { | ||
| 298 | console.error('Error getting secrets settings:', error); | ||
| 299 | return null; | ||
| 300 | } | ||
| 301 | } | ||
| 302 | |||
| 280 | async function viewSecrets() { | 303 | async function viewSecrets() { |
| 281 | const response = await fetch('/api/secrets/view', { | 304 | const response = await fetch('/api/secrets/view', { |
| 282 | method: 'POST', | 305 | method: 'POST', |
| @@ -1,5 +1,6 @@ | |||
| 1 | import { getRequestHeaders } from '../script.js'; | 1 | import { getRequestHeaders } from '../script.js'; |
| 2 | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; | 2 | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; |
| 3 | import { canViewSecrets } from './secrets.js'; | ||
| 3 | import { renderTemplateAsync } from './templates.js'; | 4 | import { renderTemplateAsync } from './templates.js'; |
| 4 | import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js'; | 5 | import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js'; |
| 5 | 6 | ||
| @@ -266,6 +267,11 @@ async function backupUserData(handle, callback) { | |||
| 266 | throw new Error('Failed to backup user data'); | 267 | throw new Error('Failed to backup user data'); |
| 267 | } | 268 | } |
| 268 | 269 | ||
| 270 | const includesSecrets = await canViewSecrets(); | ||
| 271 | if (includesSecrets === false) { | ||
| 272 | toastr.warning('The backup will not include secrets due to a server configuration.', 'Secrets Not Included'); | ||
| 273 | } | ||
| 274 | |||
| 269 | const blob = await response.blob(); | 275 | const blob = await response.blob(); |
| 270 | const header = response.headers.get('Content-Disposition'); | 276 | const header = response.headers.get('Content-Disposition'); |
| 271 | const parts = header.split(';'); | 277 | const parts = header.split(';'); |
| @@ -104,7 +104,7 @@ const EXPORTABLE_KEYS = [ | |||
| 104 | SECRET_KEYS.DEEPLX_URL, | 104 | SECRET_KEYS.DEEPLX_URL, |
| 105 | ]; | 105 | ]; |
| 106 | 106 | ||
| 107 | const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean'); | 107 | export const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean'); |
| 108 | 108 | ||
| 109 | /** | 109 | /** |
| 110 | * SecretManager class to handle all secret operations | 110 | * SecretManager class to handle all secret operations |
| @@ -636,3 +636,7 @@ router.post('/rename', (request, response) => { | |||
| 636 | return response.sendStatus(500); | 636 | return response.sendStatus(500); |
| 637 | } | 637 | } |
| 638 | }); | 638 | }); |
| 639 | |||
| 640 | router.post('/settings', async (_request, response) => { | ||
| 641 | return response.send({ allowKeysExposure }); | ||
| 642 | }); | ||
| @@ -8,7 +8,7 @@ import express from 'express'; | |||
| 8 | import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js'; | 8 | import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js'; |
| 9 | import { SETTINGS_FILE } from '../constants.js'; | 9 | import { SETTINGS_FILE } from '../constants.js'; |
| 10 | import { checkForNewContent, CONTENT_TYPES } from './content-manager.js'; | 10 | import { checkForNewContent, CONTENT_TYPES } from './content-manager.js'; |
| 11 | import { color, Cache } from '../util.js'; | 11 | import { color, Cache, getConfigValue } from '../util.js'; |
| 12 | 12 | ||
| 13 | const RESET_CACHE = new Cache(5 * 60 * 1000); | 13 | const RESET_CACHE = new Cache(5 * 60 * 1000); |
| 14 | 14 | ||
| @@ -138,6 +138,13 @@ router.post('/change-password', async (request, response) => { | |||
| 138 | 138 | ||
| 139 | router.post('/backup', async (request, response) => { | 139 | router.post('/backup', async (request, response) => { |
| 140 | try { | 140 | try { |
| 141 | const allowFullDataBackup = !!getConfigValue('backups.allowFullDataBackup', true, 'boolean'); | ||
| 142 | |||
| 143 | if (!allowFullDataBackup) { | ||
| 144 | console.warn('Backup failed: Full data backup is disabled in configuration'); | ||
| 145 | return response.status(403).json({ error: 'Full data backup is disabled' }); | ||
| 146 | } | ||
| 147 | |||
| 141 | const handle = request.body.handle; | 148 | const handle = request.body.handle; |
| 142 | 149 | ||
| 143 | if (!handle) { | 150 | if (!handle) { |
| @@ -17,7 +17,7 @@ import sanitize from 'sanitize-filename'; | |||
| 17 | 17 | ||
| 18 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; | 18 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; |
| 19 | import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent } from './util.js'; | 19 | import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent } from './util.js'; |
| 20 | import { readSecret, writeSecret } from './endpoints/secrets.js'; | 20 | import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endpoints/secrets.js'; |
| 21 | import { getContentOfType } from './endpoints/content-manager.js'; | 21 | import { getContentOfType } from './endpoints/content-manager.js'; |
| 22 | import { serverDirectory } from './server-directory.js'; | 22 | import { serverDirectory } from './server-directory.js'; |
| 23 | 23 | ||
| @@ -1052,7 +1052,14 @@ export async function createBackupArchive(handle, response) { | |||
| 1052 | archive.pipe(response); | 1052 | archive.pipe(response); |
| 1053 | 1053 | ||
| 1054 | // Append files from a sub-directory, putting its contents at the root of archive | 1054 | // Append files from a sub-directory, putting its contents at the root of archive |
| 1055 | archive.directory(directories.root, false); | 1055 | const ignore = allowKeysExposure ? [] : [SECRETS_FILE]; |
| 1056 | archive.glob('**/*', { | ||
| 1057 | cwd: directories.root, | ||
| 1058 | follow: false, | ||
| 1059 | stat: true, | ||
| 1060 | dot: true, | ||
| 1061 | ignore, | ||
| 1062 | }); | ||
| 1056 | archive.finalize(); | 1063 | archive.finalize(); |
| 1057 | } | 1064 | } |
| 1058 | 1065 | ||