backupPersistor.mjs 3.2 KB

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