diff --git a/packages/backend-core/src/db/utils.js b/packages/backend-core/src/db/utils.js index abcd1dd75c..4b64473907 100644 --- a/packages/backend-core/src/db/utils.js +++ b/packages/backend-core/src/db/utils.js @@ -30,6 +30,7 @@ const UNICODE_MAX = "\ufff0" exports.ViewNames = { USER_BY_EMAIL: "by_email", + BY_API_KEY: "by_api_key", } exports.StaticDatabases = StaticDatabases diff --git a/packages/backend-core/src/db/views.js b/packages/backend-core/src/db/views.js index fd004ca0c2..e5be8e6b40 100644 --- a/packages/backend-core/src/db/views.js +++ b/packages/backend-core/src/db/views.js @@ -1,4 +1,5 @@ const { DocumentTypes, ViewNames } = require("./utils") +const { getGlobalDB } = require("../tenancy") function DesignDoc() { return { @@ -9,7 +10,8 @@ function DesignDoc() { } } -exports.createUserEmailView = async db => { +exports.createUserEmailView = async () => { + const db = getGlobalDB() let designDoc try { designDoc = await db.get("_design/database") @@ -31,3 +33,51 @@ exports.createUserEmailView = async db => { } await db.put(designDoc) } + +exports.createApiKeyView = async () => { + const db = getGlobalDB() + let designDoc + try { + designDoc = await db.get("_design/database") + } catch (err) { + designDoc = DesignDoc() + } + const view = { + map: `function(doc) { + if (doc._id.startsWith("${DocumentTypes.DEV_INFO}") && doc.apiKey) { + emit(doc.apiKey, doc.userId) + } + }`, + } + designDoc.views = { + ...designDoc.views, + [ViewNames.BY_API_KEY]: view, + } + await db.put(designDoc) +} + +exports.queryGlobalView = async (viewName, params, db = null) => { + const CreateFuncByName = { + [ViewNames.USER_BY_EMAIL]: exports.createUserEmailView, + [ViewNames.BY_API_KEY]: exports.createApiKeyView, + } + // can pass DB in if working with something specific + if (!db) { + db = getGlobalDB() + } + try { + let response = (await db.query(`database/${viewName}`, params)).rows + response = response.map(resp => + params.include_docs ? resp.doc : resp.value + ) + return response.length <= 1 ? response[0] : response + } catch (err) { + if (err != null && err.name === "not_found") { + const createFunc = CreateFuncByName[viewName] + await createFunc() + return exports.queryGlobalView(viewName, params) + } else { + throw err + } + } +} diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index 4978f7b9dc..2d24f7fb21 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -4,6 +4,8 @@ const { getUser } = require("../cache/user") const { getSession, updateSessionTTL } = require("../security/sessions") const { buildMatcherRegex, matches } = require("./matchers") const env = require("../environment") +const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db") +const { getGlobalDB } = require("../tenancy") function finalise( ctx, @@ -16,6 +18,26 @@ function finalise( ctx.version = version } +async function checkApiKey(apiKey, populateUser) { + if (apiKey === env.INTERNAL_API_KEY) { + return { valid: true } + } + const tenantId = apiKey.split(SEPARATOR)[0] + const db = getGlobalDB(tenantId) + const userId = await queryGlobalView( + ViewNames.BY_API_KEY, + { + key: apiKey, + }, + db + ) + if (userId) { + return { valid: true, user: await getUser(userId, tenantId, populateUser) } + } else { + throw "Invalid API key" + } +} + /** * This middleware is tenancy aware, so that it does not depend on other middlewares being used. * The tenancy modules should not be used here and it should be assumed that the tenancy context @@ -79,9 +101,19 @@ module.exports = ( const apiKey = ctx.request.headers[Headers.API_KEY] const tenantId = ctx.request.headers[Headers.TENANT_ID] // this is an internal request, no user made it - if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) { - authenticated = true - internal = true + if (!authenticated && apiKey) { + const populateUser = opts.populateUser ? opts.populateUser(ctx) : null + const { valid, user: foundUser } = await checkApiKey( + apiKey, + populateUser + ) + if (valid && foundUser) { + authenticated = true + user = foundUser + } else if (valid) { + authenticated = true + internal = true + } } if (!user && tenantId) { user = { tenantId } diff --git a/packages/backend-core/src/security/apiKeys.js b/packages/backend-core/src/security/apiKeys.js new file mode 100644 index 0000000000..e90418abb8 --- /dev/null +++ b/packages/backend-core/src/security/apiKeys.js @@ -0,0 +1 @@ +exports.lookupApiKey = async () => {} diff --git a/packages/backend-core/src/utils.js b/packages/backend-core/src/utils.js index 45fb4acd55..4183fa64d5 100644 --- a/packages/backend-core/src/utils.js +++ b/packages/backend-core/src/utils.js @@ -6,7 +6,7 @@ const { } = require("./db/utils") const jwt = require("jsonwebtoken") const { options } = require("./middleware/passport/jwt") -const { createUserEmailView } = require("./db/views") +const { queryGlobalView } = require("./db/views") const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants") const { getGlobalDB, @@ -139,25 +139,11 @@ exports.getGlobalUserByEmail = async email => { if (email == null) { throw "Must supply an email address to view" } - const db = getGlobalDB() - try { - let users = ( - await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { - key: email.toLowerCase(), - include_docs: true, - }) - ).rows - users = users.map(user => user.doc) - return users.length <= 1 ? users[0] : users - } catch (err) { - if (err != null && err.name === "not_found") { - await createUserEmailView(db) - return exports.getGlobalUserByEmail(email) - } else { - throw err - } - } + return queryGlobalView(ViewNames.USER_BY_EMAIL, { + key: email.toLowerCase(), + include_docs: true, + }) } exports.saveUser = async ( diff --git a/packages/worker/src/api/controllers/global/self.js b/packages/worker/src/api/controllers/global/self.js index baf9b74e1d..4c2fa961b9 100644 --- a/packages/worker/src/api/controllers/global/self.js +++ b/packages/worker/src/api/controllers/global/self.js @@ -1,7 +1,11 @@ -const { getGlobalDB } = require("@budibase/backend-core/tenancy") -const { generateDevInfoID } = require("@budibase/backend-core/db") +const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy") +const { generateDevInfoID, SEPARATOR } = require("@budibase/backend-core/db") const { newid } = require("@budibase/backend-core/utils") +function newApiKey() { + return `${getTenantId()}${SEPARATOR}${newid()}` +} + function cleanupDevInfo(info) { // user doesn't need to aware of dev doc info delete info._id @@ -16,9 +20,9 @@ exports.generateAPIKey = async ctx => { try { devInfo = await db.get(id) } catch (err) { - devInfo = { _id: id } + devInfo = { _id: id, userId: ctx.user._id } } - devInfo.apiKey = newid() + devInfo.apiKey = newApiKey() await db.put(devInfo) ctx.body = cleanupDevInfo(devInfo) } @@ -32,7 +36,8 @@ exports.fetchAPIKey = async ctx => { } catch (err) { devInfo = { _id: id, - apiKey: newid(), + userId: ctx.user._id, + apiKey: newApiKey(), } await db.put(devInfo) }