From d5ce94d27037fa6a39c951fbd4f81632cd8ee6b3 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <5454374+emcsween@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:45:44 -0400 Subject: [PATCH] Merge pull request #12474 from overleaf/em-oauth-scripts Management scripts for OAuth client configurations GitOrigin-RevId: 4463f4716fdd060708581635fb20980e61a78df9 --- .../web/scripts/create_oauth_application.js | 50 ------- services/web/scripts/oauth/register_client.js | 128 ++++++++++++++++++ services/web/scripts/oauth/remove_client.js | 121 +++++++++++++++++ 3 files changed, 249 insertions(+), 50 deletions(-) delete mode 100644 services/web/scripts/create_oauth_application.js create mode 100644 services/web/scripts/oauth/register_client.js create mode 100644 services/web/scripts/oauth/remove_client.js diff --git a/services/web/scripts/create_oauth_application.js b/services/web/scripts/create_oauth_application.js deleted file mode 100644 index 7716cdd130..0000000000 --- a/services/web/scripts/create_oauth_application.js +++ /dev/null @@ -1,50 +0,0 @@ -const fs = require('fs') -const { OauthApplication } = require('../app/src/models/OauthApplication') -const parseArgs = require('minimist') -const OError = require('@overleaf/o-error') -const { waitForDb } = require('../app/src/infrastructure/mongodb') - -async function _loadInputDocument(inputFilePath) { - console.log(`Loading input from ${inputFilePath}`) - try { - const inputText = await fs.promises.readFile(inputFilePath, 'utf-8') - const inputDocument = JSON.parse(inputText) - return inputDocument - } catch (err) { - throw OError.tag(err, 'error loading input document') - } -} - -async function _writeOauthApplicationDocument(doc) { - console.log('Waiting for db...') - await waitForDb() - const oauthApp = new OauthApplication(doc) - console.log( - `Writing document to mongo { name: '${oauthApp.name}', id: '${oauthApp.id}' }` - ) - await oauthApp.save() -} - -async function main() { - const argv = parseArgs(process.argv.slice(2), { - string: ['file'], - unknown: function (arg) { - console.error('unrecognised argument', arg) - process.exit(1) - }, - }) - const doc = await _loadInputDocument(argv.file) - await _writeOauthApplicationDocument(doc) -} - -if (require.main === module) { - main() - .then(() => { - console.log('Done') - process.exit(0) - }) - .catch(err => { - console.error(err) - process.exit(1) - }) -} diff --git a/services/web/scripts/oauth/register_client.js b/services/web/scripts/oauth/register_client.js new file mode 100644 index 0000000000..4c4601d921 --- /dev/null +++ b/services/web/scripts/oauth/register_client.js @@ -0,0 +1,128 @@ +const minimist = require('minimist') +const { ObjectId } = require('mongodb') +const { waitForDb, db } = require('../../app/src/infrastructure/mongodb') + +async function main() { + const opts = parseArgs() + await waitForDb() + const application = await getApplication(opts.id) + if (application == null) { + console.log( + `Application ${opts.id} is not registered. Creating a new configuration.` + ) + if (opts.name == null) { + console.error('Missing --name option') + process.exit(1) + } + if (opts.secret == null) { + console.error('Missing --secret option') + process.exit(1) + } + } else { + console.log(`Updating configuration for client: ${application.name}`) + if (opts.mongoId != null) { + console.error('Cannot change Mongo ID for an existing client') + process.exit(1) + } + } + await upsertApplication(opts) +} + +async function getApplication(clientId) { + return await db.oauthApplications.findOne({ id: clientId }) +} + +async function upsertApplication(opts) { + const key = { id: opts.id } + const defaults = {} + const updates = {} + if (opts.name != null) { + updates.name = opts.name + } + if (opts.secret != null) { + updates.clientSecret = opts.secret + } + if (opts.grants != null) { + updates.grants = opts.grants + } else { + defaults.grants = [] + } + if (opts.scopes != null) { + updates.scopes = opts.scopes + } else { + defaults.scopes = [] + } + if (opts.redirectUris != null) { + updates.redirectUris = opts.redirectUris + } else { + defaults.redirectUris = [] + } + if (opts.mongoId != null) { + defaults._id = ObjectId(opts.mongoId) + } + + await db.oauthApplications.updateOne( + key, + { + $setOnInsert: { ...key, ...defaults }, + $set: updates, + }, + { upsert: true } + ) +} + +function parseArgs() { + const args = minimist(process.argv.slice(2), { + boolean: ['help'], + }) + if (args.help) { + usage() + process.exit(0) + } + if (args._.length !== 1) { + usage() + process.exit(1) + } + + return { + id: args._[0], + mongoId: args['mongo-id'], + name: args.name, + secret: args.secret, + scopes: toArray(args.scope), + grants: toArray(args.grant), + redirectUris: toArray(args['redirect-uri']), + } +} + +function usage() { + console.error(`Usage: register_client.js [OPTS...] CLIENT_ID + +Creates or updates an OAuth client configuration + +Options: + --name Descriptive name for the OAuth client (required for creation) + --secret Client secret (required for creation) + --scope Accepted scope (can be given more than once) + --grant Accepted grant type (can be given more than once) + --redirect-uri Accepted redirect URI (can be given more than once) + --mongo-id Mongo ID to use if the configuration is created (optional) +`) +} + +function toArray(value) { + if (value != null && !Array.isArray(value)) { + return [value] + } else { + return value + } +} + +main() + .then(() => { + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/services/web/scripts/oauth/remove_client.js b/services/web/scripts/oauth/remove_client.js new file mode 100644 index 0000000000..09edb4d5e2 --- /dev/null +++ b/services/web/scripts/oauth/remove_client.js @@ -0,0 +1,121 @@ +const minimist = require('minimist') +const { ReadPreference } = require('mongodb') +const { waitForDb, db } = require('../../app/src/infrastructure/mongodb') + +async function main() { + const opts = parseArgs() + await waitForDb() + const application = await getApplication(opts.clientId) + if (application == null) { + console.error(`Client configuration not found: ${opts.clientId}`) + process.exit(1) + } + if (opts.commit) { + console.log( + `Preparing to remove OAuth client configuration: ${application.name}.` + ) + + const deletedAccessTokens = await deleteAccessTokens(application._id) + console.log(`Deleted ${deletedAccessTokens} access tokens`) + + const deletedAuthorizationCodes = await deleteAuthorizationCodes( + application._id + ) + console.log(`Deleted ${deletedAuthorizationCodes} authorization codes`) + + await deleteApplication(application._id) + console.log('Deleted OAuth client configuration') + } else { + console.log( + `Preparing to remove OAuth client configuration (dry run): ${application.name}.` + ) + const accessTokenCount = await countAccessTokens(application._id) + const authorizationCodeCount = await countAuthorizationCodes( + application._id + ) + console.log( + `This would delete ${accessTokenCount} access tokens and ${authorizationCodeCount} authorization codes.` + ) + console.log('This was a dry run. Rerun with --commit to proceed.') + } +} + +async function getApplication(clientId) { + return await db.oauthApplications.findOne({ id: clientId }) +} + +async function countAccessTokens(applicationId) { + return await db.oauthAccessTokens.count( + { + oauthApplication_id: applicationId, + }, + { readPreference: ReadPreference.secondary } + ) +} + +async function countAuthorizationCodes(applicationId) { + return await db.oauthAuthorizationCodes.count( + { + oauthApplication_id: applicationId, + }, + { readPreference: ReadPreference.secondary } + ) +} + +async function deleteAccessTokens(applicationId) { + const res = await db.oauthAccessTokens.deleteMany({ + oauthApplication_id: applicationId, + }) + return res.deletedCount +} + +async function deleteAuthorizationCodes(applicationId) { + const res = await db.oauthAuthorizationCodes.deleteMany({ + oauthApplication_id: applicationId, + }) + return res.deletedCount +} + +async function deleteApplication(applicationId) { + await db.oauthApplications.deleteOne({ _id: applicationId }) +} + +function parseArgs() { + const args = minimist(process.argv.slice(2), { + boolean: ['help', 'commit'], + }) + if (args.help) { + usage() + process.exit(0) + } + if (args._.length !== 1) { + usage() + process.exit(1) + } + + return { + clientId: args._[0], + commit: args.commit, + } +} + +function usage() { + console.error(`Usage: remove_client.js [OPTS...] CLIENT_ID + +Removes an OAuth client configuration and all associated tokens and +authorization codes + +Options: + --commit Really delete the OAuth application (will do a dry run by default) + +`) +} + +main() + .then(() => { + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(1) + })