Explorar el Código

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

Management scripts for OAuth client configurations

GitOrigin-RevId: 4463f4716fdd060708581635fb20980e61a78df9
Eric Mc Sween hace 3 años
padre
commit
470dcdedb7

+ 0 - 50
services/web/scripts/create_oauth_application.js

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

+ 128 - 0
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)
+  })

+ 121 - 0
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)
+  })