mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
cefdc78c6e
commit
9e6a767c96
6 changed files with 144 additions and 18 deletions
|
@ -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": "*",
|
||||||
|
|
|
@ -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 }
|
|
@ -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
9
package-lock.json
generated
|
@ -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": "*",
|
||||||
|
|
|
@ -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": "*",
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue