Merge pull request #12474 from overleaf/em-oauth-scripts

Management scripts for OAuth client configurations

GitOrigin-RevId: 4463f4716fdd060708581635fb20980e61a78df9
This commit is contained in:
Eric Mc Sween 2023-04-03 15:45:44 -04:00 committed by Copybot
parent 9a55bbf325
commit 470dcdedb7
3 changed files with 249 additions and 50 deletions

View file

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

View file

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

View file

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