backupPersistor.mjs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. // @ts-check
  2. import fs from 'node:fs'
  3. import Path from 'node:path'
  4. import _ from 'lodash'
  5. import config from 'config'
  6. import { SecretManagerServiceClient } from '@google-cloud/secret-manager'
  7. import OError from '@overleaf/o-error'
  8. import {
  9. PerProjectEncryptedS3Persistor,
  10. RootKeyEncryptionKey,
  11. } from '@overleaf/object-persistor/src/PerProjectEncryptedS3Persistor.js'
  12. import { HistoryStore } from './history_store.js'
  13. const persistorConfig = _.cloneDeep(config.get('backupPersistor'))
  14. const { chunksBucket, deksBucket, globalBlobsBucket, projectBlobsBucket } =
  15. config.get('backupStore')
  16. export { chunksBucket, globalBlobsBucket, projectBlobsBucket }
  17. function convertKey(key, convertFn) {
  18. if (_.has(persistorConfig, key)) {
  19. _.update(persistorConfig, key, convertFn)
  20. }
  21. }
  22. convertKey('s3SSEC.httpOptions.timeout', s => parseInt(s, 10))
  23. convertKey('s3SSEC.maxRetries', s => parseInt(s, 10))
  24. convertKey('s3SSEC.pathStyle', s => s === 'true')
  25. // array of CA, either inlined or on disk
  26. convertKey('s3SSEC.ca', s =>
  27. JSON.parse(s).map(ca => (ca.startsWith('/') ? fs.readFileSync(ca) : ca))
  28. )
  29. /** @type {() => Promise<string>} */
  30. let getRawRootKeyEncryptionKeys
  31. if ((process.env.NODE_ENV || 'production') === 'production') {
  32. ;[persistorConfig.s3SSEC.key, persistorConfig.s3SSEC.secret] = (
  33. await loadFromSecretsManager(
  34. process.env.BACKUP_AWS_CREDENTIALS || '',
  35. 'BACKUP_AWS_CREDENTIALS'
  36. )
  37. ).split(':')
  38. getRawRootKeyEncryptionKeys = () =>
  39. loadFromSecretsManager(
  40. persistorConfig.keyEncryptionKeys,
  41. 'BACKUP_KEY_ENCRYPTION_KEYS'
  42. )
  43. } else {
  44. getRawRootKeyEncryptionKeys = () => persistorConfig.keyEncryptionKeys
  45. }
  46. export const DELETION_ONLY = persistorConfig.keyEncryptionKeys === 'none'
  47. if (DELETION_ONLY) {
  48. // For Backup-deleter; should not encrypt or read data; deleting does not need key.
  49. getRawRootKeyEncryptionKeys = () => new Promise(_resolve => {})
  50. }
  51. const PROJECT_FOLDER_REGEX =
  52. /^\d{3}\/\d{3}\/\d{3,}\/|[0-9a-f]{3}\/[0-9a-f]{3}\/[0-9a-f]{18}\/$/
  53. /**
  54. * @param {string} bucketName
  55. * @param {string} path
  56. * @return {string}
  57. */
  58. export function pathToProjectFolder(bucketName, path) {
  59. switch (bucketName) {
  60. case deksBucket:
  61. case chunksBucket:
  62. case projectBlobsBucket:
  63. const projectFolder = Path.join(...path.split('/').slice(0, 3)) + '/'
  64. if (!PROJECT_FOLDER_REGEX.test(projectFolder)) {
  65. throw new OError('invalid project folder', { bucketName, path })
  66. }
  67. return projectFolder
  68. default:
  69. throw new Error(`${bucketName} does not store per-project files`)
  70. }
  71. }
  72. /**
  73. * @param {string} name
  74. * @param {string} label
  75. * @return {Promise<string>}
  76. */
  77. async function loadFromSecretsManager(name, label) {
  78. const client = new SecretManagerServiceClient()
  79. const [version] = await client.accessSecretVersion({ name })
  80. if (!version.payload?.data) throw new Error(`empty secret: ${label}`)
  81. return version.payload.data.toString()
  82. }
  83. async function getRootKeyEncryptionKeys() {
  84. return JSON.parse(await getRawRootKeyEncryptionKeys()).map(
  85. ({ key, salt }) => {
  86. return new RootKeyEncryptionKey(
  87. Buffer.from(key, 'base64'),
  88. Buffer.from(salt, 'base64')
  89. )
  90. }
  91. )
  92. }
  93. export const backupPersistor = new PerProjectEncryptedS3Persistor({
  94. ...persistorConfig.s3SSEC,
  95. dataEncryptionKeyBucketName: deksBucket,
  96. pathToProjectFolder,
  97. getRootKeyEncryptionKeys,
  98. storageClass: {
  99. [deksBucket]: 'STANDARD',
  100. [chunksBucket]: persistorConfig.tieringStorageClass,
  101. [projectBlobsBucket]: persistorConfig.tieringStorageClass,
  102. },
  103. })
  104. export const backupHistoryStore = new HistoryStore(
  105. backupPersistor,
  106. chunksBucket
  107. )