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

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

GitOrigin-RevId: ce3374bb5d318a7f16a416ac1719a819c1160fb4
This commit is contained in:
Jakob Ackermann 2023-01-31 11:03:50 +00:00 committed by Copybot
parent cefdc78c6e
commit 9e6a767c96
6 changed files with 144 additions and 18 deletions

View file

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

View file

@ -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 }

View file

@ -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,
}

9
package-lock.json generated
View file

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

View file

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

View file

@ -5,6 +5,9 @@ process.env.MONGO_SOCKET_TIMEOUT =
const { ReadPreference } = require('mongodb') const { ReadPreference } = require('mongodb')
const { db, waitForDb } = require('../app/src/infrastructure/mongodb') const { db, waitForDb } = require('../app/src/infrastructure/mongodb')
const _ = require('lodash') const _ = require('lodash')
const {
formatTokenUsageStats,
} = require('@overleaf/access-token-encryptor/scripts/helpers/format-usage-stats')
const CASES = { const CASES = {
users: { users: {
@ -57,22 +60,7 @@ async function main() {
Object.assign(STATS, stats) Object.assign(STATS, stats)
} }
const prettyStats = [] formatTokenUsageStats()
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)
} }
main() main()