diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js index d8c67a5854..d5424d277b 100644 --- a/packages/auth/src/cache/user.js +++ b/packages/auth/src/cache/user.js @@ -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 diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index cf9a19e58b..303553212b 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -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) { diff --git a/packages/auth/src/middleware/matchers.js b/packages/auth/src/middleware/matchers.js index f46f0d781b..a555823136 100644 --- a/packages/auth/src/middleware/matchers.js +++ b/packages/auth/src/middleware/matchers.js @@ -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" diff --git a/packages/auth/src/middleware/tenancy.js b/packages/auth/src/middleware/tenancy.js index 9242f11c26..b80b9a6763 100644 --- a/packages/auth/src/middleware/tenancy.js +++ b/packages/auth/src/middleware/tenancy.js @@ -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() - } - - const allowQs = !!matches(ctx, allowQsOptions) - setTenantId(ctx, { allowQs }) - return next() - }) - } + return ContextFactory.getMiddleware(ctx => { + const allowNoTenant = !!matches(ctx, noTenancyOptions) + const allowQs = !!matches(ctx, allowQsOptions) + setTenantId(ctx, { allowQs, allowNoTenant }) + }) } diff --git a/packages/auth/src/tenancy/FunctionContext.js b/packages/auth/src/tenancy/FunctionContext.js new file mode 100644 index 0000000000..06e0d92f5c --- /dev/null +++ b/packages/auth/src/tenancy/FunctionContext.js @@ -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 diff --git a/packages/auth/src/tenancy/context.js b/packages/auth/src/tenancy/context.js index 5210700bd0..71fba2a3d5 100644 --- a/packages/auth/src/tenancy/context.js +++ b/packages/auth/src/tenancy/context.js @@ -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") } diff --git a/packages/auth/src/tenancy/tenancy.js b/packages/auth/src/tenancy/tenancy.js index 16dedd3ebf..6e18ea7154 100644 --- a/packages/auth/src/tenancy/tenancy.js +++ b/packages/auth/src/tenancy/tenancy.js @@ -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") diff --git a/packages/builder/src/pages/builder/portal/manage/email/index.svelte b/packages/builder/src/pages/builder/portal/manage/email/index.svelte index 5161625510..f94d2bcd0f 100644 --- a/packages/builder/src/pages/builder/portal/manage/email/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/email/index.svelte @@ -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", diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 15c4c9bf7a..d0de611d74 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -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(), } diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 13fb7efd1d..81601eea1a 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -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) diff --git a/packages/server/src/automations/steps/sendSmtpEmail.js b/packages/server/src/automations/steps/sendSmtpEmail.js index 7b25da801e..764972b402 100644 --- a/packages/server/src/automations/steps/sendSmtpEmail.js +++ b/packages/server/src/automations/steps/sendSmtpEmail.js @@ -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 = "