Implementing some changes to how context gets set for tenancy, after testing, as well as updating server.

This commit is contained in:
mike12345567 2021-08-03 15:32:25 +01:00
parent 75ae7ac8d6
commit 54e765a182
18 changed files with 194 additions and 152 deletions

View File

@ -1,22 +1,25 @@
const redis = require("../redis/authRedis")
const {
updateTenantId,
getTenantId,
lookupTenantId,
getGlobalDB,
isTenantIdSet,
} = require("../tenancy")
const EXPIRY_SECONDS = 3600
exports.getUser = async userId => {
if (!isTenantIdSet()) {
updateTenantId(await lookupTenantId(userId))
exports.getUser = async (userId, tenantId = null) => {
if (!tenantId) {
try {
tenantId = getTenantId()
} catch (err) {
tenantId = await lookupTenantId(userId)
}
}
const client = await redis.getUserClient()
// try cache
let user = await client.get(userId)
if (!user) {
user = await getGlobalDB().get(userId)
user = await getGlobalDB(tenantId).get(userId)
client.store(userId, user, EXPIRY_SECONDS)
}
return user

View File

@ -3,7 +3,6 @@ const { getCookie, clearCookie } = require("../utils")
const { getUser } = require("../cache/user")
const { getSession, updateSessionTTL } = require("../security/sessions")
const { buildMatcherRegex, matches } = require("./matchers")
const { isTenantIdSet, updateTenantId } = require("../tenancy")
const env = require("../environment")
function finalise(
@ -17,6 +16,11 @@ function finalise(
ctx.version = version
}
/**
* 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
* has not yet been populated.
*/
module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
return async (ctx, next) => {
@ -42,10 +46,7 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
error = "No session found"
} else {
try {
if (session.tenantId && !isTenantIdSet()) {
updateTenantId(session.tenantId)
}
user = await getUser(userId)
user = await getUser(userId, session.tenantId)
delete user.password
authenticated = true
} catch (err) {

View File

@ -1,6 +1,9 @@
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g
exports.buildMatcherRegex = patterns => {
if (!patterns) {
return []
}
return patterns.map(pattern => {
const isObj = typeof pattern === "object" && pattern.route
const method = isObj ? pattern.method : "GET"

View File

@ -1,20 +1,14 @@
const { createTenancyContext, setTenantId } = require("../tenancy")
const { setTenantId } = require("../tenancy")
const ContextFactory = require("../tenancy/FunctionContext")
const { buildMatcherRegex, matches } = require("./matchers")
module.exports = (allowQueryStringPatterns, noTenancyPatterns) => {
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
return (ctx, next) => {
// always run in context
return createTenancyContext().runAndReturn(() => {
if (matches(ctx, noTenancyOptions)) {
return next()
}
return ContextFactory.getMiddleware(ctx => {
const allowNoTenant = !!matches(ctx, noTenancyOptions)
const allowQs = !!matches(ctx, allowQsOptions)
setTenantId(ctx, { allowQs })
return next()
setTenantId(ctx, { allowQs, allowNoTenant })
})
}
}

View File

@ -0,0 +1,69 @@
const cls = require("cls-hooked")
const { newid } = require("../hashing")
const REQUEST_ID_KEY = "requestId"
class FunctionContext {
static getMiddleware(updateCtxFn = null) {
const namespace = this.createNamespace()
return async function(ctx, next) {
await new Promise(namespace.bind(function(resolve, reject) {
// store a contextual request ID that can be used anywhere (audit logs)
namespace.set(REQUEST_ID_KEY, newid())
namespace.bindEmitter(ctx.req)
namespace.bindEmitter(ctx.res)
if (updateCtxFn) {
updateCtxFn(ctx)
}
next().then(resolve).catch(reject)
}))
}
}
static run(callback) {
const namespace = this.createNamespace()
return namespace.runAndReturn(callback)
}
static setOnContext(key, value) {
const namespace = this.createNamespace()
namespace.set(key, value)
}
static getContextStorage() {
if (this._namespace && this._namespace.active) {
const { id, _ns_name, ...contextData } = this._namespace.active
return contextData
}
return {}
}
static getFromContext(key) {
const context = this.getContextStorage()
if (context) {
return context[key]
} else {
return null
}
}
static destroyNamespace() {
if (this._namespace) {
cls.destroyNamespace("session")
this._namespace = null
}
}
static createNamespace() {
if (!this._namespace) {
this._namespace = cls.createNamespace("session")
}
return this._namespace
}
}
module.exports = FunctionContext

View File

@ -1,6 +1,6 @@
const cls = require("cls-hooked")
const env = require("../environment")
const { Headers } = require("../../constants")
const cls = require("./FunctionContext")
exports.DEFAULT_TENANT_ID = "default"
@ -12,66 +12,61 @@ exports.isMultiTenant = () => {
return env.MULTI_TENANCY
}
// continuation local storage
const CONTEXT_NAME = "tenancy"
const TENANT_ID = "tenantId"
exports.createTenancyContext = () => {
return cls.createNamespace(CONTEXT_NAME)
}
const getTenancyContext = () => {
return cls.getNamespace(CONTEXT_NAME)
}
// used for automations, API endpoints should always be in context already
exports.doInTenant = (tenantId, task) => {
const context = getTenancyContext()
return getTenancyContext().runAndReturn(() => {
return cls.run(() => {
// set the tenant id
context.set(TENANT_ID, tenantId)
cls.setOnContext(TENANT_ID, tenantId)
// invoke the task
const result = task()
// clear down the tenant id manually for extra safety
// this should also happen automatically when the call exits
context.set(TENANT_ID, null)
cls.setOnContext(TENANT_ID, null)
return result
})
}
exports.updateTenantId = tenantId => {
getTenancyContext().set(TENANT_ID, tenantId)
cls.setOnContext(TENANT_ID, tenantId)
}
exports.setTenantId = (ctx, opts = { allowQs: false }) => {
exports.setTenantId = (ctx, opts = { allowQs: false, allowNoTenant: false }) => {
let tenantId
// exit early if not multi-tenant
if (!exports.isMultiTenant()) {
getTenancyContext().set(TENANT_ID, this.DEFAULT_TENANT_ID)
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID)
return
}
const params = ctx.request.params || {}
const allowQs = opts && opts.allowQs
const allowNoTenant = opts && opts.allowNoTenant
const header = ctx.request.headers[Headers.TENANT_ID]
const user = ctx.request.user || {}
tenantId = user.tenantId || params.tenantId || header
if (opts.allowQs && !tenantId) {
const user = ctx.user || {}
if (allowQs) {
const query = ctx.request.query || {}
tenantId = query.tenantId
}
// override query string (if allowed) by user, or header
// URL params cannot be used in a middleware, as they are
// processed later in the chain
tenantId = user.tenantId || header || tenantId
if (!tenantId) {
if (!tenantId && !allowNoTenant) {
ctx.throw(403, "Tenant id not set")
}
getTenancyContext().set(TENANT_ID, tenantId)
// check tenant ID just incase no tenant was allowed
if (tenantId) {
cls.setOnContext(TENANT_ID, tenantId)
}
}
exports.isTenantIdSet = () => {
const tenantId = getTenancyContext().get(TENANT_ID)
const tenantId = cls.getFromContext(TENANT_ID)
return !!tenantId
}
@ -79,7 +74,7 @@ exports.getTenantId = () => {
if (!exports.isMultiTenant()) {
return exports.DEFAULT_TENANT_ID
}
const tenantId = getTenancyContext().get(TENANT_ID)
const tenantId = cls.getFromContext(TENANT_ID)
if (!tenantId) {
throw Error("Tenant id not found")
}

View File

@ -1,4 +1,4 @@
const { getDB } = require("../../db")
const { getDB } = require("../db")
const { SEPARATOR, StaticDatabases } = require("../db/constants")
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context")
const env = require("../environment")

View File

@ -94,6 +94,9 @@
requireAuth = smtpConfig.config.auth != null
// always attach the auth for the forms purpose -
// this will be removed later if required
if (!smtpDoc.config) {
smtpDoc.config = {}
}
if (!smtpDoc.config.auth) {
smtpConfig.config.auth = {
type: "login",

View File

@ -38,6 +38,7 @@ const {
backupClientLibrary,
revertClientLibrary,
} = require("../../utilities/fileSystem/clientLibrary")
const { getTenantId, isMultiTenant } = require("@budibase/auth/tenancy")
const URL_REGEX_SLASH = /\/|\\/g
@ -92,8 +93,9 @@ async function getAppUrlIfNotInUse(ctx) {
return url
}
async function createInstance(tenantId, template) {
const baseAppId = generateAppID(env.MULTI_TENANCY ? tenantId : null)
async function createInstance(template) {
const tenantId = isMultiTenant() ? getTenantId() : null
const baseAppId = generateAppID(tenantId)
const appId = generateDevAppID(baseAppId)
const db = new CouchDB(appId)
@ -128,8 +130,7 @@ async function createInstance(tenantId, template) {
exports.fetch = async function (ctx) {
const dev = ctx.query && ctx.query.status === AppStatus.DEV
const all = ctx.query && ctx.query.status === AppStatus.ALL
const tenantId = ctx.user.tenantId
const apps = await getAllApps(CouchDB, { tenantId, dev, all })
const apps = await getAllApps(CouchDB, { dev, all })
// get the locks for all the dev apps
if (dev || all) {
@ -189,7 +190,6 @@ exports.fetchAppPackage = async function (ctx) {
}
exports.create = async function (ctx) {
const tenantId = ctx.user.tenantId
const { useTemplate, templateKey } = ctx.request.body
const instanceConfig = {
useTemplate,
@ -198,7 +198,7 @@ exports.create = async function (ctx) {
if (ctx.request.files && ctx.request.files.templateFile) {
instanceConfig.file = ctx.request.files.templateFile
}
const instance = await createInstance(tenantId, instanceConfig)
const instance = await createInstance(instanceConfig)
const appId = instance._id
const url = await getAppUrlIfNotInUse(ctx)
@ -222,7 +222,7 @@ exports.create = async function (ctx) {
url: url,
template: ctx.request.body.template,
instance: instance,
tenantId,
tenantId: getTenantId(),
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
}

View File

@ -10,6 +10,13 @@ const env = require("../environment")
const router = new Router()
const NO_TENANCY_ENDPOINTS = [
{
route: "/api/analytics",
method: "GET",
},
]
router
.use(
compress({
@ -32,12 +39,13 @@ router
})
.use("/health", ctx => (ctx.status = 200))
.use("/version", ctx => (ctx.body = pkg.version))
.use(buildTenancyMiddleware())
.use(
buildAuthMiddleware(null, {
publicAllowed: true,
})
)
// nothing in the server should allow query string tenants
.use(buildTenancyMiddleware(null, NO_TENANCY_ENDPOINTS))
.use(currentApp)
.use(auditLog)

View File

@ -46,13 +46,13 @@ module.exports.definition = {
},
}
module.exports.run = async function ({ inputs, tenantId }) {
module.exports.run = async function ({ inputs }) {
let { to, from, subject, contents } = inputs
if (!contents) {
contents = "<h1>No content</h1>"
}
try {
let response = await sendSmtpEmail(tenantId, to, from, subject, contents)
let response = await sendSmtpEmail(to, from, subject, contents)
return {
success: true,
response,

View File

@ -6,6 +6,7 @@ const { processObject } = require("@budibase/string-templates")
const { DEFAULT_TENANT_ID } = require("@budibase/auth").constants
const CouchDB = require("../db")
const { DocumentTypes } = require("../db/utils")
const { doInTenant } = require("@budibase/auth/tenancy")
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
@ -56,7 +57,7 @@ class Orchestrator {
async execute() {
let automation = this._automation
const app = this.getApp()
const app = await this.getApp()
for (let step of automation.definition.steps) {
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
step.inputs = await processObject(step.inputs, this._context)
@ -66,13 +67,15 @@ class Orchestrator {
)
// appId is always passed
try {
const outputs = await stepFn({
let tenantId = app.tenantId || DEFAULT_TENANT_ID
const outputs = await doInTenant(tenantId, () => {
return stepFn({
inputs: step.inputs,
appId: this._appId,
apiKey: automation.apiKey,
emitter: this._emitter,
context: this._context,
tenantId: app.tenantId || DEFAULT_TENANT_ID,
})
})
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
break

View File

@ -68,5 +68,6 @@ module.exports = async (ctx, next) => {
) {
setCookie(ctx, { appId }, Cookies.CurrentApp)
}
return next()
}

View File

@ -33,7 +33,9 @@ function processUser(appId, user) {
}
exports.getCachedSelf = async (ctx, appId) => {
const user = await userCache.getUser(ctx.user._id, ctx.user.tenantId)
// this has to be tenant aware, can't depend on the context to find it out
// running some middlewares before the tenancy causes context to break
const user = await userCache.getUser(ctx.user._id)
return processUser(appId, user)
}

View File

@ -4,6 +4,7 @@ const { checkSlashesInUrl } = require("./index")
const { getDeployedAppID } = require("@budibase/auth/db")
const { updateAppRole, getGlobalUser } = require("./global")
const { Headers } = require("@budibase/auth/constants")
const { getTenantId, isTenantIdSet } = require("@budibase/auth/tenancy")
function request(ctx, request) {
if (!request.headers) {
@ -11,6 +12,9 @@ function request(ctx, request) {
}
if (!ctx) {
request.headers[Headers.API_KEY] = env.INTERNAL_API_KEY
if (isTenantIdSet()) {
request.headers[Headers.TENANT_ID] = getTenantId()
}
}
if (request.body && Object.keys(request.body).length > 0) {
request.headers["Content-Type"] = "application/json"
@ -29,13 +33,14 @@ function request(ctx, request) {
exports.request = request
exports.sendSmtpEmail = async (tenantId, to, from, subject, contents) => {
// have to pass in the tenant ID as this could be coming from an automation
exports.sendSmtpEmail = async (to, from, subject, contents) => {
// tenant ID will be set in header
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
request(null, {
method: "POST",
body: {
tenantId,
email: to,
from,
contents,

View File

@ -13,65 +13,9 @@ const { user: userCache } = require("@budibase/auth/cache")
const { invalidateSessions } = require("@budibase/auth/sessions")
const CouchDB = require("../../../db")
const env = require("../../../environment")
const { getGlobalDB, getTenantId } = require("@budibase/auth/tenancy")
const { getGlobalDB, getTenantId, doesTenantExist, tryAddTenant, updateTenantId } = require("@budibase/auth/tenancy")
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
async function tryAddTenant(tenantId, userId, email) {
const db = new CouchDB(PLATFORM_INFO_DB)
const getDoc = async id => {
if (!id) {
return null
}
try {
return await db.get(id)
} catch (err) {
return { _id: id }
}
}
let [tenants, userIdDoc, emailDoc] = await Promise.all([
getDoc(TENANT_DOC),
getDoc(userId),
getDoc(email),
])
if (!Array.isArray(tenants.tenantIds)) {
tenants = {
_id: TENANT_DOC,
tenantIds: [],
}
}
let promises = []
if (userIdDoc) {
userIdDoc.tenantId = tenantId
promises.push(db.put(userIdDoc))
}
if (emailDoc) {
emailDoc.tenantId = tenantId
promises.push(db.put(emailDoc))
}
if (tenants.tenantIds.indexOf(tenantId) === -1) {
tenants.tenantIds.push(tenantId)
promises.push(db.put(tenants))
}
await Promise.all(promises)
}
async function doesTenantExist(tenantId) {
const db = new CouchDB(PLATFORM_INFO_DB)
let tenants
try {
tenants = await db.get(TENANT_DOC)
} catch (err) {
// if theres an error the doc doesn't exist, no tenants exist
return false
}
return (
tenants &&
Array.isArray(tenants.tenantIds) &&
tenants.tenantIds.indexOf(tenantId) !== -1
)
}
async function allUsers() {
const db = getGlobalDB()
@ -87,6 +31,8 @@ async function saveUser(user, tenantId) {
if (!tenantId) {
throw "No tenancy specified."
}
// need to set the context for this request, as specified
updateTenantId(tenantId)
// specify the tenancy incase we're making a new admin user (public)
const db = getGlobalDB(tenantId)
let { email, password, _id } = user
@ -162,7 +108,7 @@ exports.adminUser = async ctx => {
ctx.throw(403, "Organisation already exists.")
}
const db = getGlobalDB()
const db = getGlobalDB(tenantId)
const response = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,

View File

@ -5,17 +5,6 @@ const { routes } = require("./routes")
const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } =
require("@budibase/auth").auth
const NO_TENANCY_ENDPOINTS = [
{
route: "/api/system",
method: "ALL",
},
{
route: "/api/global/users/self",
method: "GET",
},
]
const PUBLIC_ENDPOINTS = [
{
// this covers all of the POST auth routes
@ -32,10 +21,6 @@ const PUBLIC_ENDPOINTS = [
route: "/api/global/configs/public",
method: "GET",
},
{
route: "api/global/flags",
method: "GET",
},
{
route: "/api/global/configs/checklist",
method: "GET",
@ -48,6 +33,22 @@ const PUBLIC_ENDPOINTS = [
route: "/api/global/users/invite/accept",
method: "POST",
},
{
route: "api/system/flags",
method: "GET",
},
]
const NO_TENANCY_ENDPOINTS = [
...PUBLIC_ENDPOINTS,
{
route: "/api/system",
method: "ALL",
},
{
route: "/api/global/users/self",
method: "GET",
},
]
const router = new Router()
@ -65,8 +66,8 @@ router
})
)
.use("/health", ctx => (ctx.status = 200))
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
// for now no public access is allowed to worker (bar health check)
.use((ctx, next) => {
if (!ctx.isAuthenticated && !ctx.publicEndpoint) {

View File

@ -2,6 +2,7 @@ const Router = require("@koa/router")
const authController = require("../../controllers/global/auth")
const joiValidator = require("../../../middleware/joi-validator")
const Joi = require("joi")
const { updateTenantId } = require("@budibase/auth/tenancy")
const router = Router()
@ -28,34 +29,41 @@ function buildResetUpdateValidation() {
}).required().unknown(false))
}
function updateTenant(ctx, next) {
updateTenantId(ctx.params.tenantId)
return next()
}
router
.post(
"/api/global/auth/:tenantId/login",
buildAuthValidation(),
updateTenant,
authController.authenticate
)
.post(
"/api/global/auth/:tenantId/reset",
buildResetValidation(),
updateTenant,
authController.reset
)
.post(
"/api/global/auth/:tenantId/reset/update",
buildResetUpdateValidation(),
updateTenant,
authController.resetUpdate
)
.post("/api/global/auth/logout", authController.logout)
.get("/api/global/auth/:tenantId/google", authController.googlePreAuth)
.get("/api/global/auth/:tenantId/google/callback", authController.googleAuth)
.get("/api/global/auth/:tenantId/google", updateTenant, authController.googlePreAuth)
.get("/api/global/auth/:tenantId/google/callback", updateTenant, authController.googleAuth)
.get(
"/api/global/auth/:tenantId/oidc/configs/:configId",
updateTenant,
authController.oidcPreAuth
)
.get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth)
.get("/api/global/auth/:tenantId/oidc/callback", updateTenant, authController.oidcAuth)
// deprecated - used by the default system before tenancy
.get("/api/admin/auth/google/callback", authController.googleAuth)
.get("/api/global/auth/google/callback", authController.googleAuth)
.get("/api/admin/auth/oidc/callback", authController.oidcAuth)
.get("/api/global/auth/oidc/callback", authController.oidcAuth)
module.exports = router