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 | 166 | ## BACKUP CONFIGURATION |
| 167 | 167 | backups: |
| 168 | + # Allow users to create a full backup archive of their data | |
| 169 | + allowFullDataBackup: true | |
| 168 | 170 | # Common settings for all backup types |
| 169 | 171 | common: |
| 170 | 172 | # Number of backups to keep for each chat and settings file |
| @@ -40,7 +40,7 @@ declare global { | ||
| 40 | 40 | /** |
| 41 | 41 | * Authenticated user handle. |
| 42 | 42 | */ |
| 43 | 43 | handle: string | null; |
| 44 | 44 | /** |
| 45 | 45 | * Last time the session was extended. |
| 46 | 46 | */ |
| @@ -277,6 +277,29 @@ function getActiveSecretLabel(key) { | ||
| 277 | 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 | 303 | async function viewSecrets() { |
| 281 | 304 | const response = await fetch('/api/secrets/view', { |
| 282 | 305 | method: 'POST', |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { getRequestHeaders } from '../script.js'; |
| 2 | 2 | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; |
| 3 | +import { canViewSecrets } from './secrets.js'; | |
| 3 | 4 | import { renderTemplateAsync } from './templates.js'; |
| 4 | 5 | import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js'; |
| 5 | 6 | |
| @@ -266,6 +267,11 @@ async function backupUserData(handle, callback) { | ||
| 266 | 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 | 275 | const blob = await response.blob(); |
| 270 | 276 | const header = response.headers.get('Content-Disposition'); |
| 271 | 277 | const parts = header.split(';'); |
| @@ -104,7 +104,7 @@ const EXPORTABLE_KEYS = [ | ||
| 104 | 104 | SECRET_KEYS.DEEPLX_URL, |
| 105 | 105 | ]; |
| 106 | 106 | |
| 107 | 107 | export const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean'); |
| 108 | 108 | |
| 109 | 109 | /** |
| 110 | 110 | * SecretManager class to handle all secret operations |
| @@ -636,3 +636,7 @@ router.post('/rename', (request, response) => { | ||
| 636 | 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 | 8 | import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js'; |
| 9 | 9 | import { SETTINGS_FILE } from '../constants.js'; |
| 10 | 10 | import { checkForNewContent, CONTENT_TYPES } from './content-manager.js'; |
| 11 | 11 | import { color, Cache, getConfigValue } from '../util.js'; |
| 12 | 12 | |
| 13 | 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 | 139 | router.post('/backup', async (request, response) => { |
| 140 | 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 | 148 | const handle = request.body.handle; |
| 142 | 149 | |
| 143 | 150 | if (!handle) { |
| @@ -17,7 +17,7 @@ import sanitize from 'sanitize-filename'; | ||
| 17 | 17 | |
| 18 | 18 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; |
| 19 | 19 | import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent } from './util.js'; |
| 20 | 20 | import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endpoints/secrets.js'; |
| 21 | 21 | import { getContentOfType } from './endpoints/content-manager.js'; |
| 22 | 22 | import { serverDirectory } from './server-directory.js'; |
| 23 | 23 | |
| @@ -1052,7 +1052,14 @@ export async function createBackupArchive(handle, response) { | ||
| 1052 | 1052 | archive.pipe(response); |
| 1053 | 1053 | |
| 1054 | 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 | 1063 | archive.finalize(); |
| 1057 | 1064 | } |
| 1058 | 1065 | |