Kaynağa Gözat

Merge pull request #11544 from overleaf/jpa-re-encrypt-access-tokens

[misc] add scripts for rotating all the encrypted access-tokens

GitOrigin-RevId: ce3374bb5d318a7f16a416ac1719a819c1160fb4
Jakob Ackermann 3 yıl önce
ebeveyn
işleme
9e6a767c96

+ 5 - 1
libraries/access-token-encryptor/package.json

@@ -13,8 +13,12 @@
   },
   "author": "",
   "license": "AGPL-3.0-only",
+  "dependencies": {
+    "lodash": "^4.17.21"
+  },
   "peerDependencies": {
-    "@overleaf/logger": "*"
+    "@overleaf/logger": "*",
+    "mongodb": "*"
   },
   "devDependencies": {
     "@overleaf/logger": "*",

+ 20 - 0
libraries/access-token-encryptor/scripts/helpers/format-usage-stats.js

@@ -0,0 +1,20 @@
+function formatTokenUsageStats(STATS) {
+  const prettyStats = []
+  const sortedStats = Object.entries(STATS).sort((a, b) =>
+    a[0] > b[0] ? 1 : -1
+  )
+  const totalByName = {}
+  for (const [key, n] of sortedStats) {
+    const [name, version, collectionName, path, label] = key.split(':')
+    totalByName[name] = (totalByName[name] || 0) + n
+    prettyStats.push({ name, version, collectionName, path, label, n })
+  }
+  for (const row of prettyStats) {
+    row.percentage = ((100 * row.n) / totalByName[row.name])
+      .toFixed(2)
+      .padStart(6)
+  }
+  console.table(prettyStats)
+}
+
+module.exports = { formatTokenUsageStats }

+ 106 - 0
libraries/access-token-encryptor/scripts/helpers/re-encrypt-tokens.js

@@ -0,0 +1,106 @@
+const { ReadPreference } = require('mongodb')
+const _ = require('lodash')
+const { formatTokenUsageStats } = require('./format-usage-stats')
+
+const LOG_EVERY_IN_S = parseInt(process.env.LOG_EVERY_IN_S || '5', 10)
+const DRY_RUN = !process.argv.includes('--dry-run=false')
+
+/**
+ * @param {AccessTokenEncryptor} accessTokenEncryptor
+ * @param {string} encryptedJson
+ * @return {Promise<string>}
+ */
+async function reEncryptTokens(accessTokenEncryptor, encryptedJson) {
+  return new Promise((resolve, reject) => {
+    accessTokenEncryptor.decryptToJson(encryptedJson, (err, json) => {
+      if (err) return reject(err)
+      accessTokenEncryptor.encryptJson(json, (err, reEncryptedJson) => {
+        if (err) return reject(err)
+        resolve(reEncryptedJson)
+      })
+    })
+  })
+}
+
+/**
+ * @param {AccessTokenEncryptor} accessTokenEncryptor
+ * @param {Collection} collection
+ * @param {Object} paths
+ * @return {Promise<{}>}
+ */
+async function reEncryptTokensInCollection({
+  accessTokenEncryptor,
+  collection,
+  paths,
+}) {
+  const { collectionName } = collection
+  const stats = {}
+
+  let processed = 0
+  let updatedNUsers = 0
+  let lastLog = 0
+  const logProgress = () => {
+    if (DRY_RUN) {
+      console.warn(
+        `processed ${processed} | Would have updated ${updatedNUsers} users`
+      )
+    } else {
+      console.warn(`processed ${processed} | Updated ${updatedNUsers} users`)
+    }
+  }
+
+  const projection = { _id: 1 }
+  for (const path of Object.values(paths)) {
+    projection[path] = 1
+  }
+  const cursor = collection.find(
+    {},
+    {
+      readPreference: ReadPreference.SECONDARY,
+      projection,
+    }
+  )
+
+  for await (const doc of cursor) {
+    processed++
+
+    let update = null
+    for (const [name, path] of Object.entries(paths)) {
+      const blob = _.get(doc, path)
+      if (!blob) continue
+      // Schema: LABEL:SALT:CIPHERTEXT:IV
+      const [label, , , iv] = blob.split(':', 4)
+      const version = iv ? 'v2' : 'v1'
+
+      const key = [name, version, collectionName, path, label].join(':')
+      stats[key] = (stats[key] || 0) + 1
+
+      if (version === 'v1') {
+        update = update || {}
+        update[path] = await reEncryptTokens(accessTokenEncryptor, blob)
+      }
+    }
+
+    if (Date.now() - lastLog >= LOG_EVERY_IN_S * 1000) {
+      logProgress()
+      lastLog = Date.now()
+    }
+    if (update) {
+      updatedNUsers++
+
+      const { _id } = doc
+      if (DRY_RUN) {
+        console.log('Would upgrade tokens for user', _id, Object.keys(update))
+      } else {
+        console.log('Upgrading tokens for user', _id, Object.keys(update))
+        await collection.updateOne({ _id }, { $set: update })
+      }
+    }
+  }
+  logProgress()
+  formatTokenUsageStats(stats)
+}
+
+module.exports = {
+  reEncryptTokensInCollection,
+}

+ 8 - 1
package-lock.json

@@ -80,6 +80,9 @@
       "name": "@overleaf/access-token-encryptor",
       "version": "2.2.0",
       "license": "AGPL-3.0-only",
+      "dependencies": {
+        "lodash": "^4.17.21"
+      },
       "devDependencies": {
         "@overleaf/logger": "*",
         "bunyan": "^1.8.15",
@@ -90,7 +93,8 @@
         "sinon": "^9.2.4"
       },
       "peerDependencies": {
-        "@overleaf/logger": "*"
+        "@overleaf/logger": "*",
+        "mongodb": "*"
       }
     },
     "libraries/logger": {
@@ -34322,6 +34326,7 @@
         "@opentelemetry/sdk-trace-base": "^1.2.0",
         "@opentelemetry/sdk-trace-web": "^1.2.0",
         "@opentelemetry/semantic-conventions": "^1.2.0",
+        "@overleaf/access-token-encryptor": "*",
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
         "@overleaf/o-error": "*",
@@ -41978,6 +41983,7 @@
         "@overleaf/logger": "*",
         "bunyan": "^1.8.15",
         "chai": "^4.3.6",
+        "lodash": "^4.17.21",
         "mocha": "^10.2.0",
         "nock": "0.15.2",
         "sandboxed-module": "^2.0.4",
@@ -43467,6 +43473,7 @@
         "@opentelemetry/sdk-trace-base": "^1.2.0",
         "@opentelemetry/sdk-trace-web": "^1.2.0",
         "@opentelemetry/semantic-conventions": "^1.2.0",
+        "@overleaf/access-token-encryptor": "*",
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
         "@overleaf/o-error": "*",

+ 1 - 0
services/web/package.json

@@ -97,6 +97,7 @@
     "@opentelemetry/sdk-trace-base": "^1.2.0",
     "@opentelemetry/sdk-trace-web": "^1.2.0",
     "@opentelemetry/semantic-conventions": "^1.2.0",
+    "@overleaf/access-token-encryptor": "*",
     "@overleaf/logger": "*",
     "@overleaf/metrics": "*",
     "@overleaf/o-error": "*",

+ 4 - 16
services/web/scripts/count_encrypted_access_tokens.js

@@ -5,6 +5,9 @@ process.env.MONGO_SOCKET_TIMEOUT =
 const { ReadPreference } = require('mongodb')
 const { db, waitForDb } = require('../app/src/infrastructure/mongodb')
 const _ = require('lodash')
+const {
+  formatTokenUsageStats,
+} = require('@overleaf/access-token-encryptor/scripts/helpers/format-usage-stats')
 
 const CASES = {
   users: {
@@ -57,22 +60,7 @@ async function main() {
     Object.assign(STATS, stats)
   }
 
-  const prettyStats = []
-  const sortedStats = Object.entries(STATS).sort((a, b) =>
-    a[0] > b[0] ? 1 : -1
-  )
-  const totalByName = {}
-  for (const [key, n] of sortedStats) {
-    const [name, version, collectionName, path, label] = key.split(':')
-    totalByName[name] = (totalByName[name] || 0) + n
-    prettyStats.push({ name, version, collectionName, path, label, n })
-  }
-  for (const row of prettyStats) {
-    row.percentage = ((100 * row.n) / totalByName[row.name])
-      .toFixed(2)
-      .padStart(6)
-  }
-  console.table(prettyStats)
+  formatTokenUsageStats()
 }
 
 main()