Adding tenancy to the API key, making the authenticated middleware aware of new user API keys, using a view to lookup the user by API key.

This commit is contained in:
mike12345567 2022-02-11 22:24:48 +00:00
parent d296ec4ef8
commit 84bf8c3422
6 changed files with 103 additions and 28 deletions

View File

@ -30,6 +30,7 @@ const UNICODE_MAX = "\ufff0"
exports.ViewNames = { exports.ViewNames = {
USER_BY_EMAIL: "by_email", USER_BY_EMAIL: "by_email",
BY_API_KEY: "by_api_key",
} }
exports.StaticDatabases = StaticDatabases exports.StaticDatabases = StaticDatabases

View File

@ -1,4 +1,5 @@
const { DocumentTypes, ViewNames } = require("./utils") const { DocumentTypes, ViewNames } = require("./utils")
const { getGlobalDB } = require("../tenancy")
function DesignDoc() { function DesignDoc() {
return { return {
@ -9,7 +10,8 @@ function DesignDoc() {
} }
} }
exports.createUserEmailView = async db => { exports.createUserEmailView = async () => {
const db = getGlobalDB()
let designDoc let designDoc
try { try {
designDoc = await db.get("_design/database") designDoc = await db.get("_design/database")
@ -31,3 +33,51 @@ exports.createUserEmailView = async db => {
} }
await db.put(designDoc) 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
}
}
}

View File

@ -4,6 +4,8 @@ const { getUser } = require("../cache/user")
const { getSession, updateSessionTTL } = require("../security/sessions") const { getSession, updateSessionTTL } = require("../security/sessions")
const { buildMatcherRegex, matches } = require("./matchers") const { buildMatcherRegex, matches } = require("./matchers")
const env = require("../environment") const env = require("../environment")
const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db")
const { getGlobalDB } = require("../tenancy")
function finalise( function finalise(
ctx, ctx,
@ -16,6 +18,26 @@ function finalise(
ctx.version = version 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. * 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 * The tenancy modules should not be used here and it should be assumed that the tenancy context
@ -79,10 +101,20 @@ module.exports = (
const apiKey = ctx.request.headers[Headers.API_KEY] const apiKey = ctx.request.headers[Headers.API_KEY]
const tenantId = ctx.request.headers[Headers.TENANT_ID] const tenantId = ctx.request.headers[Headers.TENANT_ID]
// this is an internal request, no user made it // this is an internal request, no user made it
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) { 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 authenticated = true
internal = true internal = true
} }
}
if (!user && tenantId) { if (!user && tenantId) {
user = { tenantId } user = { tenantId }
} }

View File

@ -0,0 +1 @@
exports.lookupApiKey = async () => {}

View File

@ -6,7 +6,7 @@ const {
} = require("./db/utils") } = require("./db/utils")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt") 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 { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants")
const { const {
getGlobalDB, getGlobalDB,
@ -139,25 +139,11 @@ exports.getGlobalUserByEmail = async email => {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
const db = getGlobalDB()
try { return queryGlobalView(ViewNames.USER_BY_EMAIL, {
let users = (
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
key: email.toLowerCase(), key: email.toLowerCase(),
include_docs: true, 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
}
}
} }
exports.saveUser = async ( exports.saveUser = async (

View File

@ -1,7 +1,11 @@
const { getGlobalDB } = require("@budibase/backend-core/tenancy") const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy")
const { generateDevInfoID } = require("@budibase/backend-core/db") const { generateDevInfoID, SEPARATOR } = require("@budibase/backend-core/db")
const { newid } = require("@budibase/backend-core/utils") const { newid } = require("@budibase/backend-core/utils")
function newApiKey() {
return `${getTenantId()}${SEPARATOR}${newid()}`
}
function cleanupDevInfo(info) { function cleanupDevInfo(info) {
// user doesn't need to aware of dev doc info // user doesn't need to aware of dev doc info
delete info._id delete info._id
@ -16,9 +20,9 @@ exports.generateAPIKey = async ctx => {
try { try {
devInfo = await db.get(id) devInfo = await db.get(id)
} catch (err) { } catch (err) {
devInfo = { _id: id } devInfo = { _id: id, userId: ctx.user._id }
} }
devInfo.apiKey = newid() devInfo.apiKey = newApiKey()
await db.put(devInfo) await db.put(devInfo)
ctx.body = cleanupDevInfo(devInfo) ctx.body = cleanupDevInfo(devInfo)
} }
@ -32,7 +36,8 @@ exports.fetchAPIKey = async ctx => {
} catch (err) { } catch (err) {
devInfo = { devInfo = {
_id: id, _id: id,
apiKey: newid(), userId: ctx.user._id,
apiKey: newApiKey(),
} }
await db.put(devInfo) await db.put(devInfo)
} }