From 7a69dcef78229e86115f5f8d7fdc8f61f3be336d Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 2 Aug 2021 18:34:43 +0100 Subject: [PATCH] re-write, to use the ideas that Rory put in place, still WIP, un-tested but all implemented. --- packages/auth/db.js | 5 +- packages/auth/package.json | 1 + packages/auth/src/cache/user.js | 16 ++- packages/auth/src/db/constants.js | 17 +++ packages/auth/src/db/utils.js | 61 +--------- packages/auth/src/index.js | 7 +- packages/auth/src/middleware/authenticated.js | 39 ++----- packages/auth/src/middleware/index.js | 2 + packages/auth/src/middleware/matchers.js | 30 +++++ .../auth/src/middleware/passport/local.js | 9 +- .../middleware/passport/third-party-common.js | 11 +- packages/auth/src/middleware/tenancy.js | 23 ++++ packages/auth/src/tenancy/context.js | 87 +++++++++++++++ packages/auth/src/tenancy/index.js | 4 + packages/auth/src/tenancy/tenancy.js | 105 ++++++++++++++++++ packages/auth/src/utils.js | 28 +---- packages/auth/tenancy.js | 1 + packages/auth/yarn.lock | 35 +++++- .../server/src/api/controllers/apikeys.js | 17 +-- packages/server/src/api/index.js | 3 +- .../src/tests/utilities/TestConfiguration.js | 2 +- packages/server/src/utilities/global.js | 7 +- .../worker/src/api/controllers/global/auth.js | 34 +++--- .../src/api/controllers/global/configs.js | 23 ++-- .../src/api/controllers/global/email.js | 10 +- .../src/api/controllers/global/templates.js | 17 +-- .../src/api/controllers/global/users.js | 39 +++---- .../src/api/controllers/global/workspaces.js | 10 +- packages/worker/src/api/index.js | 14 ++- .../worker/src/constants/templates/index.js | 19 +--- packages/worker/src/utilities/email.js | 21 ++-- packages/worker/src/utilities/templates.js | 22 ++-- 32 files changed, 461 insertions(+), 258 deletions(-) create mode 100644 packages/auth/src/db/constants.js create mode 100644 packages/auth/src/middleware/matchers.js create mode 100644 packages/auth/src/middleware/tenancy.js create mode 100644 packages/auth/src/tenancy/context.js create mode 100644 packages/auth/src/tenancy/index.js create mode 100644 packages/auth/src/tenancy/tenancy.js create mode 100644 packages/auth/tenancy.js diff --git a/packages/auth/db.js b/packages/auth/db.js index 4b03ec36cc..a7b38821a7 100644 --- a/packages/auth/db.js +++ b/packages/auth/db.js @@ -1 +1,4 @@ -module.exports = require("./src/db/utils") +module.exports = { + ...require("./src/db/utils"), + ...require("./src/db/constants"), +} diff --git a/packages/auth/package.json b/packages/auth/package.json index 986e4da18d..2bcf581c41 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -13,6 +13,7 @@ "@techpass/passport-openidconnect": "^0.3.0", "aws-sdk": "^2.901.0", "bcryptjs": "^2.4.3", + "cls-hooked": "^4.2.2", "ioredis": "^4.27.1", "jsonwebtoken": "^8.5.1", "koa-passport": "^4.1.4", diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js index 616612a588..d8c67a5854 100644 --- a/packages/auth/src/cache/user.js +++ b/packages/auth/src/cache/user.js @@ -1,18 +1,22 @@ -const { getGlobalDB } = require("../db/utils") const redis = require("../redis/authRedis") -const { lookupTenantId } = require("../utils") +const { + updateTenantId, + lookupTenantId, + getGlobalDB, + isTenantIdSet, +} = require("../tenancy") const EXPIRY_SECONDS = 3600 -exports.getUser = async (userId, tenantId = null) => { - if (!tenantId) { - tenantId = await lookupTenantId(userId) +exports.getUser = async userId => { + if (!isTenantIdSet()) { + updateTenantId(await lookupTenantId(userId)) } const client = await redis.getUserClient() // try cache let user = await client.get(userId) if (!user) { - user = await getGlobalDB(tenantId).get(userId) + user = await getGlobalDB().get(userId) client.store(userId, user, EXPIRY_SECONDS) } return user diff --git a/packages/auth/src/db/constants.js b/packages/auth/src/db/constants.js new file mode 100644 index 0000000000..227d793f3e --- /dev/null +++ b/packages/auth/src/db/constants.js @@ -0,0 +1,17 @@ +exports.SEPARATOR = "_" + +exports.StaticDatabases = { + GLOBAL: { + name: "global-db", + docs: { + apiKeys: "apikeys", + }, + }, + // contains information about tenancy and so on + PLATFORM_INFO: { + name: "global-info", + docs: { + tenants: "tenants", + }, + }, +} \ No newline at end of file diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 9b6f8fcbed..e53cb01123 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -3,29 +3,16 @@ const Replication = require("./Replication") const { getDB } = require("./index") const { DEFAULT_TENANT_ID } = require("../constants") const env = require("../environment") +const { StaticDatabases, SEPARATOR } = require("./constants") +const { getTenantId } = require("../tenancy") const UNICODE_MAX = "\ufff0" -const SEPARATOR = "_" exports.ViewNames = { USER_BY_EMAIL: "by_email", } -exports.StaticDatabases = { - GLOBAL: { - name: "global-db", - docs: { - apiKeys: "apikeys", - }, - }, - // contains information about tenancy and so on - PLATFORM_INFO: { - name: "global-info", - docs: { - tenants: "tenants", - }, - }, -} +exports.StaticDatabases = StaticDatabases const PRE_APP = "app" const PRE_DEV = "dev" @@ -74,45 +61,6 @@ function getDocParams(docType, docId = null, otherProps = {}) { } } -/** - * Gets the name of the global DB to connect to in a multi-tenancy system. - */ -exports.getGlobalDB = tenantId => { - // fallback for system pre multi-tenancy - let dbName = exports.StaticDatabases.GLOBAL.name - if (tenantId && tenantId !== DEFAULT_TENANT_ID) { - dbName = `${tenantId}${SEPARATOR}${dbName}` - } - if (env.MULTI_TENANCY && tenantId == null) { - throw "Cannot create global DB without tenantId" - } - return getDB(dbName) -} - -/** - * Given a koa context this tries to extra what tenant is being accessed. - */ -exports.getTenantIdFromCtx = (ctx, opts = { includeQuery: false }) => { - if (!ctx) { - return null - } - const user = ctx.user || {} - const params = ctx.request.params || {} - let query = {} - if (opts && opts.includeQuery) { - query = ctx.request.query || {} - } - return user.tenantId || params.tenantId || query.tenantId -} - -/** - * Given a koa context this tries to find the correct tenant Global DB. - */ -exports.getGlobalDBFromCtx = (ctx, opts) => { - const tenantId = exports.getTenantIdFromCtx(ctx, opts) - return exports.getGlobalDB(tenantId) -} - /** * Generates a new workspace ID. * @returns {string} The new workspace ID which the workspace doc can be stored under. @@ -216,7 +164,8 @@ exports.getDeployedAppID = appId => { * different users/companies apps as there is no security around it - all apps are returned. * @return {Promise} returns the app information document stored in each app database. */ -exports.getAllApps = async (CouchDB, { tenantId, dev, all } = {}) => { +exports.getAllApps = async (CouchDB, { dev, all } = {}) => { + let tenantId = getTenantId() if (!env.MULTI_TENANCY && !tenantId) { tenantId = DEFAULT_TENANT_ID } diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index f13cb5d868..5421dea214 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -1,7 +1,8 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -const { getGlobalDB, StaticDatabases } = require("./db/utils") +const { StaticDatabases } = require("./db/utils") +const { getGlobalDB } = require("./tenancy") const { jwt, local, @@ -9,6 +10,7 @@ const { google, oidc, auditLog, + tenancy, } = require("./middleware") const { setDB } = require("./db") const userCache = require("./cache/user") @@ -20,7 +22,7 @@ passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) passport.serializeUser((user, done) => done(null, user)) passport.deserializeUser(async (user, done) => { - const db = getGlobalDB(user.tenantId) + const db = getGlobalDB() try { const user = await db.get(user._id) @@ -54,6 +56,7 @@ module.exports = { google, oidc, jwt: require("jsonwebtoken"), + buildTenancyMiddleware: tenancy, auditLog, }, cache: { diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index fa28ca7ba5..cf9a19e58b 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -2,27 +2,10 @@ const { Cookies, Headers } = require("../constants") 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") -const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g - -function buildNoAuthRegex(patterns) { - return patterns.map(pattern => { - const isObj = typeof pattern === "object" && pattern.route - const method = isObj ? pattern.method : "GET" - let route = isObj ? pattern.route : pattern - - const matches = route.match(PARAM_REGEX) - if (matches) { - for (let match of matches) { - const pattern = "/.*" + (match.endsWith("/") ? "/" : "") - route = route.replace(match, pattern) - } - } - return { regex: new RegExp(route), method } - }) -} - function finalise( ctx, { authenticated, user, internal, version, publicEndpoint } = {} @@ -34,19 +17,14 @@ function finalise( ctx.version = version } -module.exports = (noAuthPatterns = [], opts) => { - const noAuthOptions = noAuthPatterns ? buildNoAuthRegex(noAuthPatterns) : [] +module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => { + const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] return async (ctx, next) => { let publicEndpoint = false const version = ctx.request.headers[Headers.API_VER] // the path is not authenticated - const found = noAuthOptions.find(({ regex, method }) => { - return ( - regex.test(ctx.request.url) && - ctx.request.method.toLowerCase() === method.toLowerCase() - ) - }) - if (found != null) { + const found = matches(ctx, noAuthOptions) + if (found) { publicEndpoint = true } try { @@ -64,7 +42,10 @@ module.exports = (noAuthPatterns = [], opts) => { error = "No session found" } else { try { - user = await getUser(userId, session.tenantId) + if (session.tenantId && !isTenantIdSet()) { + updateTenantId(session.tenantId) + } + user = await getUser(userId) delete user.password authenticated = true } catch (err) { diff --git a/packages/auth/src/middleware/index.js b/packages/auth/src/middleware/index.js index 35c7d9c388..689859a139 100644 --- a/packages/auth/src/middleware/index.js +++ b/packages/auth/src/middleware/index.js @@ -4,6 +4,7 @@ const google = require("./passport/google") const oidc = require("./passport/oidc") const authenticated = require("./authenticated") const auditLog = require("./auditLog") +const tenancy = require("./tenancy") module.exports = { google, @@ -12,4 +13,5 @@ module.exports = { local, authenticated, auditLog, + tenancy, } diff --git a/packages/auth/src/middleware/matchers.js b/packages/auth/src/middleware/matchers.js new file mode 100644 index 0000000000..79b14bc99d --- /dev/null +++ b/packages/auth/src/middleware/matchers.js @@ -0,0 +1,30 @@ +const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g + +exports.buildMatcherRegex = patterns => { + return patterns.map(pattern => { + const isObj = typeof pattern === "object" && pattern.route + const method = isObj ? pattern.method : "GET" + let route = isObj ? pattern.route : pattern + + const matches = route.match(PARAM_REGEX) + if (matches) { + for (let match of matches) { + const pattern = "/.*" + (match.endsWith("/") ? "/" : "") + route = route.replace(match, pattern) + } + } + return { regex: new RegExp(route), method } + }) +} + +exports.matches = (ctx, options) => { + return options.find(({ regex, method }) => { + const urlMatch = regex.test(ctx.request.url) + const methodMatch = + method === "ALL" + ? true + : ctx.request.method.toLowerCase() === method.toLowerCase() + + return urlMatch && methodMatch + }) +} \ No newline at end of file diff --git a/packages/auth/src/middleware/passport/local.js b/packages/auth/src/middleware/passport/local.js index 22bcf51fc2..0db40d64eb 100644 --- a/packages/auth/src/middleware/passport/local.js +++ b/packages/auth/src/middleware/passport/local.js @@ -1,11 +1,12 @@ const jwt = require("jsonwebtoken") -const { UserStatus, DEFAULT_TENANT_ID } = require("../../constants") +const { UserStatus } = require("../../constants") const { compare } = require("../../hashing") const env = require("../../environment") const { getGlobalUserByEmail } = require("../../utils") const { authError } = require("./utils") const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") +const { getTenantId } = require("../../tenancy") const INVALID_ERR = "Invalid Credentials" @@ -24,11 +25,8 @@ exports.options = { exports.authenticate = async function (ctx, email, password, done) { if (!email) return authError(done, "Email Required") if (!password) return authError(done, "Password Required") - const params = ctx.params || {} - // use the request to find the tenantId - let tenantId = params.tenantId || DEFAULT_TENANT_ID - const dbUser = await getGlobalUserByEmail(email, tenantId) + const dbUser = await getGlobalUserByEmail(email) if (dbUser == null) { return authError(done, "User not found") } @@ -41,6 +39,7 @@ exports.authenticate = async function (ctx, email, password, done) { // authenticate if (await compare(password, dbUser.password)) { const sessionId = newid() + const tenantId = getTenantId() await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index 094161ec7f..7490cc4031 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -1,10 +1,11 @@ const env = require("../../environment") const jwt = require("jsonwebtoken") -const { generateGlobalUserID, getGlobalDB } = require("../../db/utils") +const { generateGlobalUserID } = require("../../db/utils") const { authError } = require("./utils") const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") -const { getGlobalUserByEmail, lookupTenantId } = require("../../utils") +const { getGlobalUserByEmail } = require("../../utils") +const { getGlobalDB, getTenantId } = require("../../tenancy") /** * Common authentication logic for third parties. e.g. OAuth, OIDC. @@ -26,8 +27,7 @@ exports.authenticateThirdParty = async function ( // use the third party id const userId = generateGlobalUserID(thirdPartyUser.userId) - const tenantId = await lookupTenantId(userId) - const db = getGlobalDB(tenantId) + const db = getGlobalDB() let dbUser @@ -47,7 +47,7 @@ exports.authenticateThirdParty = async function ( // fallback to loading by email if (!dbUser) { - dbUser = await getGlobalUserByEmail(thirdPartyUser.email, tenantId) + dbUser = await getGlobalUserByEmail(thirdPartyUser.email) } // exit early if there is still no user and auto creation is disabled @@ -75,6 +75,7 @@ exports.authenticateThirdParty = async function ( // authenticate const sessionId = newid() + const tenantId = getTenantId() await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( diff --git a/packages/auth/src/middleware/tenancy.js b/packages/auth/src/middleware/tenancy.js new file mode 100644 index 0000000000..3a1df833c1 --- /dev/null +++ b/packages/auth/src/middleware/tenancy.js @@ -0,0 +1,23 @@ +const { + createTenancyContext, + setTenantId, +} = require("../tenancy") +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() + }) + } +} diff --git a/packages/auth/src/tenancy/context.js b/packages/auth/src/tenancy/context.js new file mode 100644 index 0000000000..1331dd0a35 --- /dev/null +++ b/packages/auth/src/tenancy/context.js @@ -0,0 +1,87 @@ +const cls = require("cls-hooked") +const env = require("../environment") +const { Headers } = require("../../constants") + +exports.DEFAULT_TENANT_ID = "default" + +exports.isDefaultTenant = () => { + return exports.getTenantId() === exports.DEFAULT_TENANT_ID +} + +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(() => { + // set the tenant id + context.set(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) + + return result + }) +} + +exports.updateTenantId = tenantId => { + getTenancyContext().set(TENANT_ID, tenantId) +} + +exports.setTenantId = (ctx, opts = { allowQs: false }) => { + let tenantId + // exit early if not multi-tenant + if (!exports.isMultiTenant()) { + getTenancyContext().set(TENANT_ID, this.DEFAULT_TENANT_ID) + return + } + + const params = ctx.request.params || {} + const header = ctx.request.headers[Headers.TENANT_ID] + const user = ctx.request.user || {} + tenantId = user.tenantId || params.tenantId || header + if (opts.allowQs && !tenantId) { + const query = ctx.request.query || {} + tenantId = query.tenantId + } + + if (!tenantId) { + ctx.throw(403, "Tenant id not set") + } + + getTenancyContext().set(TENANT_ID, tenantId) +} + +exports.isTenantIdSet = () => { + const tenantId = getTenancyContext().get(TENANT_ID) + return !!tenantId +} + +exports.getTenantId = () => { + if (!exports.isMultiTenant()) { + return exports.DEFAULT_TENANT_ID + } + const tenantId = getTenancyContext().get(TENANT_ID) + if (!tenantId) { + throw Error("Tenant id not found") + } + return tenantId +} \ No newline at end of file diff --git a/packages/auth/src/tenancy/index.js b/packages/auth/src/tenancy/index.js new file mode 100644 index 0000000000..2fe257d885 --- /dev/null +++ b/packages/auth/src/tenancy/index.js @@ -0,0 +1,4 @@ +module.exports = { + ...require("./context"), + ...require("./tenancy"), +} diff --git a/packages/auth/src/tenancy/tenancy.js b/packages/auth/src/tenancy/tenancy.js new file mode 100644 index 0000000000..3c1c145938 --- /dev/null +++ b/packages/auth/src/tenancy/tenancy.js @@ -0,0 +1,105 @@ +const { getDB } = require("../../db") +const { SEPARATOR, StaticDatabases } = require("../db/constants") +const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context") +const env = require("../environment") + +const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants +const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name + +exports.addTenantToUrl = url => { + const tenantId = getTenantId() + + if (isMultiTenant()) { + const char = url.indexOf("?") === -1 ? "?" : "&" + url += `${char}tenantId=${tenantId}` + } + + return url +} + +exports.doesTenantExist = async tenantId => { + const db = getDB(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 + ) +} + +exports.tryAddTenant = async (tenantId, userId, email) => { + const db = getDB(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) +} + +exports.getGlobalDB = (tenantId = null) => { + // tenant ID can be set externally, for example user API where + // new tenants are being created, this may be the case + if (!tenantId) { + const tenantId = getTenantId() + } + + let dbName + + if (tenantId === DEFAULT_TENANT_ID) { + dbName = StaticDatabases.GLOBAL.name + } else { + dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` + } + + return getDB(dbName) +} + +exports.lookupTenantId = async userId => { + const db = getDB(StaticDatabases.PLATFORM_INFO.name) + let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null + try { + const doc = await db.get(userId) + if (doc && doc.tenantId) { + tenantId = doc.tenantId + } + } catch (err) { + // just return the default + } + return tenantId +} \ No newline at end of file diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index dd03f132a1..eee9de72fe 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -2,15 +2,12 @@ const { DocumentTypes, SEPARATOR, ViewNames, - StaticDatabases, } = require("./db/utils") const jwt = require("jsonwebtoken") const { options } = require("./middleware/passport/jwt") const { createUserEmailView } = require("./db/views") -const { getDB } = require("./db") -const { getGlobalDB } = require("./db/utils") -const { DEFAULT_TENANT_ID, Headers } = require("./constants") -const env = require("./environment") +const { Headers } = require("./constants") +const { getGlobalDB } = require("./tenancy") const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -103,32 +100,17 @@ exports.isClient = ctx => { return ctx.headers[Headers.TYPE] === "client" } -exports.lookupTenantId = async userId => { - const db = getDB(StaticDatabases.PLATFORM_INFO.name) - let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null - try { - const doc = await db.get(userId) - if (doc && doc.tenantId) { - tenantId = doc.tenantId - } - } catch (err) { - // just return the default - } - return tenantId -} - /** * Given an email address this will use a view to search through * all the users to find one with this email address. * @param {string} email the email to lookup the user by. - * @param {string|null} tenantId If tenant ID is known it can be specified * @return {Promise} */ -exports.getGlobalUserByEmail = async (email, tenantId) => { +exports.getGlobalUserByEmail = async email => { if (email == null) { throw "Must supply an email address to view" } - const db = getGlobalDB(tenantId) + const db = getGlobalDB() try { let users = ( await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { @@ -141,7 +123,7 @@ exports.getGlobalUserByEmail = async (email, tenantId) => { } catch (err) { if (err != null && err.name === "not_found") { await createUserEmailView(db) - return exports.getGlobalUserByEmail(email, tenantId) + return exports.getGlobalUserByEmail(email) } else { throw err } diff --git a/packages/auth/tenancy.js b/packages/auth/tenancy.js new file mode 100644 index 0000000000..9ca808b74e --- /dev/null +++ b/packages/auth/tenancy.js @@ -0,0 +1 @@ +module.exports = require("./src/tenancy") diff --git a/packages/auth/yarn.lock b/packages/auth/yarn.lock index 8957ecb0fc..b6be8ad1e8 100644 --- a/packages/auth/yarn.lock +++ b/packages/auth/yarn.lock @@ -798,6 +798,13 @@ ast-types@0.9.6: resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= +async-hook-jl@^1.7.6: + version "1.7.6" + resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68" + integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg== + dependencies: + stack-chain "^1.3.7" + async@~2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" @@ -1144,6 +1151,15 @@ clone-buffer@1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= +cls-hooked@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908" + integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw== + dependencies: + async-hook-jl "^1.7.6" + emitter-listener "^1.0.1" + semver "^5.4.1" + cluster-key-slot@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" @@ -1444,6 +1460,13 @@ electron-to-chromium@^1.3.723: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082" integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q== +emitter-listener@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" + integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ== + dependencies: + shimmer "^1.2.0" + emittery@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" @@ -4035,7 +4058,7 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -4096,6 +4119,11 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +shimmer@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -4250,6 +4278,11 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stack-chain@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285" + integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU= + stack-utils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" diff --git a/packages/server/src/api/controllers/apikeys.js b/packages/server/src/api/controllers/apikeys.js index 7144788945..9b1ddee4c4 100644 --- a/packages/server/src/api/controllers/apikeys.js +++ b/packages/server/src/api/controllers/apikeys.js @@ -1,9 +1,10 @@ -const { StaticDatabases, getGlobalDBFromCtx } = require("@budibase/auth/db") +const { StaticDatabases } = require("@budibase/auth/db") +const { getGlobalDB } = require("@budibase/auth/tenancy") const KEYS_DOC = StaticDatabases.GLOBAL.docs.apiKeys -async function getBuilderMainDoc(ctx) { - const db = getGlobalDBFromCtx(ctx) +async function getBuilderMainDoc() { + const db = getGlobalDB() try { return await db.get(KEYS_DOC) } catch (err) { @@ -14,16 +15,16 @@ async function getBuilderMainDoc(ctx) { } } -async function setBuilderMainDoc(ctx, doc) { +async function setBuilderMainDoc(doc) { // make sure to override the ID doc._id = KEYS_DOC - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() return db.put(doc) } exports.fetch = async function (ctx) { try { - const mainDoc = await getBuilderMainDoc(ctx) + const mainDoc = await getBuilderMainDoc() ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {} } catch (err) { /* istanbul ignore next */ @@ -36,12 +37,12 @@ exports.update = async function (ctx) { const value = ctx.request.body.value try { - const mainDoc = await getBuilderMainDoc(ctx) + const mainDoc = await getBuilderMainDoc() if (mainDoc.apiKeys == null) { mainDoc.apiKeys = {} } mainDoc.apiKeys[key] = value - const resp = await setBuilderMainDoc(ctx, mainDoc) + const resp = await setBuilderMainDoc(mainDoc) ctx.body = { _id: resp.id, _rev: resp.rev, diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 6c4188a5dc..8c940a5a50 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -1,5 +1,5 @@ const Router = require("@koa/router") -const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth +const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } = require("@budibase/auth").auth const currentApp = require("../middleware/currentapp") const compress = require("koa-compress") const zlib = require("zlib") @@ -31,6 +31,7 @@ router }) .use("/health", ctx => (ctx.status = 200)) .use("/version", ctx => (ctx.body = pkg.version)) + .use(buildTenancyMiddleware()) .use( buildAuthMiddleware(null, { publicAllowed: true, diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index ad49b0ef2e..0eb3851d98 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -18,7 +18,7 @@ const { cleanup } = require("../../utilities/fileSystem") const { Cookies, Headers } = require("@budibase/auth").constants const { jwt } = require("@budibase/auth").auth const auth = require("@budibase/auth") -const { getGlobalDB } = require("@budibase/auth/db") +const { getGlobalDB } = require("@budibase/auth/tenancy") const { createASession } = require("@budibase/auth/sessions") const { user: userCache } = require("@budibase/auth/cache") const CouchDB = require("../../db") diff --git a/packages/server/src/utilities/global.js b/packages/server/src/utilities/global.js index 2dbb956d33..d3e9701b62 100644 --- a/packages/server/src/utilities/global.js +++ b/packages/server/src/utilities/global.js @@ -3,9 +3,10 @@ const { getGlobalIDFromUserMetadataID, } = require("../db/utils") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") -const { getDeployedAppID, getGlobalDBFromCtx } = require("@budibase/auth/db") +const { getDeployedAppID } = require("@budibase/auth/db") const { getGlobalUserParams } = require("@budibase/auth/db") const { user: userCache } = require("@budibase/auth/cache") +const { getGlobalDB } = require("@budibase/auth/tenancy") exports.updateAppRole = (appId, user) => { if (!user.roles) { @@ -37,13 +38,13 @@ exports.getCachedSelf = async (ctx, appId) => { } exports.getGlobalUser = async (ctx, appId, userId) => { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() let user = await db.get(getGlobalIDFromUserMetadataID(userId)) return processUser(appId, user) } exports.getGlobalUsers = async (ctx, appId = null, users = null) => { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() let globalUsers if (users) { const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id)) diff --git a/packages/worker/src/api/controllers/global/auth.js b/packages/worker/src/api/controllers/global/auth.js index 1275084687..4f02838282 100644 --- a/packages/worker/src/api/controllers/global/auth.js +++ b/packages/worker/src/api/controllers/global/auth.js @@ -8,13 +8,13 @@ const { setCookie, getCookie, clearCookie, getGlobalUserByEmail, hash } = const { Cookies } = authPkg.constants const { passport } = authPkg.auth const { checkResetPasswordCode } = require("../../../utilities/redis") -const { getGlobalDB } = authPkg.db +const { getGlobalDB, getTenantId, isMultiTenant } = require("@budibase/auth/tenancy") const env = require("../../../environment") -function googleCallbackUrl(tenantId = null) { +function googleCallbackUrl() { let callbackUrl = `/api/global/auth` - if (tenantId) { - callbackUrl += `/${tenantId}` + if (isMultiTenant()) { + callbackUrl += `/${getTenantId()}` } callbackUrl += `/google/callback` return callbackUrl @@ -57,8 +57,7 @@ exports.authenticate = async (ctx, next) => { */ exports.reset = async ctx => { const { email } = ctx.request.body - const tenantId = ctx.params.tenantId - const configured = await isEmailConfigured(tenantId) + const configured = await isEmailConfigured() if (!configured) { ctx.throw( 400, @@ -66,10 +65,10 @@ exports.reset = async ctx => { ) } try { - const user = await getGlobalUserByEmail(email, tenantId) + const user = await getGlobalUserByEmail(email) // only if user exists, don't error though if they don't if (user) { - await sendEmail(tenantId, email, EmailTemplatePurpose.PASSWORD_RECOVERY, { + await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { user, subject: "{{ company }} platform password reset", }) @@ -90,7 +89,7 @@ exports.resetUpdate = async ctx => { const { resetCode, password } = ctx.request.body try { const userId = await checkResetPasswordCode(resetCode) - const db = getGlobalDB(ctx.params.tenantId) + const db = getGlobalDB() const user = await db.get(userId) user.password = await hash(password) await db.put(user) @@ -112,9 +111,8 @@ exports.logout = async ctx => { * On a successful login, you will be redirected to the googleAuth callback route. */ exports.googlePreAuth = async (ctx, next) => { - const tenantId = ctx.params ? ctx.params.tenantId : null - const db = getGlobalDB(tenantId) - let callbackUrl = googleCallbackUrl(tenantId) + const db = getGlobalDB() + let callbackUrl = googleCallbackUrl() const config = await authPkg.db.getScopedConfig(db, { type: Configs.GOOGLE, @@ -128,9 +126,8 @@ exports.googlePreAuth = async (ctx, next) => { } exports.googleAuth = async (ctx, next) => { - const tenantId = ctx.params ? ctx.params.tenantId : null - const db = getGlobalDB(tenantId) - const callbackUrl = googleCallbackUrl(tenantId) + const db = getGlobalDB() + const callbackUrl = googleCallbackUrl() const config = await authPkg.db.getScopedConfig(db, { type: Configs.GOOGLE, @@ -150,8 +147,7 @@ exports.googleAuth = async (ctx, next) => { } async function oidcStrategyFactory(ctx, configId) { - const tenantId = ctx.params ? ctx.params.tenantId : null - const db = getGlobalDB(ctx.params.tenantId) + const db = getGlobalDB() const config = await authPkg.db.getScopedConfig(db, { type: Configs.OIDC, group: ctx.query.group, @@ -161,8 +157,8 @@ async function oidcStrategyFactory(ctx, configId) { const protocol = env.NODE_ENV === "production" ? "https" : "http" let callbackUrl = `${protocol}://${ctx.host}/api/global/auth` - if (tenantId) { - callbackUrl += `/${tenantId}` + if (isMultiTenant()) { + callbackUrl += `/${getTenantId()}` } callbackUrl += `/oidc/callback` diff --git a/packages/worker/src/api/controllers/global/configs.js b/packages/worker/src/api/controllers/global/configs.js index 000ce85381..8b4807b684 100644 --- a/packages/worker/src/api/controllers/global/configs.js +++ b/packages/worker/src/api/controllers/global/configs.js @@ -3,17 +3,16 @@ const { getConfigParams, getGlobalUserParams, getScopedFullConfig, - getGlobalDBFromCtx, - getTenantIdFromCtx, getAllApps, } = require("@budibase/auth/db") const { Configs } = require("../../../constants") const email = require("../../../utilities/email") const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore const CouchDB = require("../../../db") +const { getGlobalDB } = require("@budibase/auth/tenancy") exports.save = async function (ctx) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const { type, workspace, user, config } = ctx.request.body // Config does not exist yet @@ -49,7 +48,7 @@ exports.save = async function (ctx) { } exports.fetch = async function (ctx) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const response = await db.allDocs( getConfigParams( { type: ctx.params.type }, @@ -66,7 +65,7 @@ exports.fetch = async function (ctx) { * The hierarchy is type -> workspace -> user. */ exports.find = async function (ctx) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const { userId, workspaceId } = ctx.query if (workspaceId && userId) { @@ -99,7 +98,7 @@ exports.find = async function (ctx) { } exports.publicOidc = async function (ctx) { - const db = getGlobalDBFromCtx(ctx, { includeQuery: true }) + const db = getGlobalDB() try { // Find the config with the most granular scope based on context const oidcConfig = await getScopedFullConfig(db, { @@ -121,7 +120,7 @@ exports.publicOidc = async function (ctx) { } exports.publicSettings = async function (ctx) { - const db = getGlobalDBFromCtx(ctx, { includeQuery: true }) + const db = getGlobalDB() try { // Find the config with the most granular scope based on context @@ -186,7 +185,7 @@ exports.upload = async function (ctx) { // add to configuration structure // TODO: right now this only does a global level - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() let cfgStructure = await getScopedFullConfig(db, { type }) if (!cfgStructure) { cfgStructure = { @@ -206,7 +205,7 @@ exports.upload = async function (ctx) { } exports.destroy = async function (ctx) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const { id, rev } = ctx.params try { @@ -218,15 +217,13 @@ exports.destroy = async function (ctx) { } exports.configChecklist = async function (ctx) { - // include the query string only for a select few endpoints - const tenantId = getTenantIdFromCtx(ctx, { includeQuery: true }) - const db = getGlobalDBFromCtx(ctx, { includeQuery: true }) + const db = getGlobalDB() try { // TODO: Watch get started video // Apps exist - const apps = await getAllApps(CouchDB, { tenantId }) + const apps = await getAllApps(CouchDB) // They have set up SMTP const smtpConfig = await getScopedFullConfig(db, { diff --git a/packages/worker/src/api/controllers/global/email.js b/packages/worker/src/api/controllers/global/email.js index 11841d1b56..50e7acdb1a 100644 --- a/packages/worker/src/api/controllers/global/email.js +++ b/packages/worker/src/api/controllers/global/email.js @@ -1,9 +1,8 @@ const { sendEmail } = require("../../../utilities/email") -const { getGlobalDBFromCtx } = require("@budibase/auth/db") +const { getGlobalDB } = require("@budibase/auth/tenancy") exports.sendEmail = async ctx => { let { - tenantId, workspaceId, email, userId, @@ -14,13 +13,10 @@ exports.sendEmail = async ctx => { } = ctx.request.body let user if (userId) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() user = await db.get(userId) } - if (!tenantId && ctx.user.tenantId) { - tenantId = ctx.user.tenantId - } - const response = await sendEmail(tenantId, email, purpose, { + const response = await sendEmail(email, purpose, { workspaceId, user, contents, diff --git a/packages/worker/src/api/controllers/global/templates.js b/packages/worker/src/api/controllers/global/templates.js index cf9e988b6c..0dc2b8abab 100644 --- a/packages/worker/src/api/controllers/global/templates.js +++ b/packages/worker/src/api/controllers/global/templates.js @@ -1,13 +1,14 @@ -const { generateTemplateID, getGlobalDBFromCtx } = require("@budibase/auth/db") +const { generateTemplateID } = require("@budibase/auth/db") const { TemplateMetadata, TemplateBindings, GLOBAL_OWNER, } = require("../../../constants") -const { getTemplatesCtx } = require("../../../constants/templates") +const { getTemplates } = require("../../../constants/templates") +const { getGlobalDB } = require("@budibase/auth/tenancy") exports.save = async ctx => { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() let template = ctx.request.body if (!template.ownerId) { template.ownerId = GLOBAL_OWNER @@ -45,29 +46,29 @@ exports.definitions = async ctx => { } exports.fetch = async ctx => { - ctx.body = await getTemplatesCtx(ctx) + ctx.body = await getTemplates() } exports.fetchByType = async ctx => { - ctx.body = await getTemplatesCtx(ctx, { + ctx.body = await getTemplates({ type: ctx.params.type, }) } exports.fetchByOwner = async ctx => { - ctx.body = await getTemplatesCtx(ctx, { + ctx.body = await getTemplates({ ownerId: ctx.params.ownerId, }) } exports.find = async ctx => { - ctx.body = await getTemplatesCtx(ctx, { + ctx.body = await getTemplates({ id: ctx.params.id, }) } exports.destroy = async ctx => { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() await db.remove(ctx.params.id, ctx.params.rev) ctx.message = `Template ${ctx.params.id} deleted.` ctx.status = 200 diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js index f6bf76c9c1..3153328ace 100644 --- a/packages/worker/src/api/controllers/global/users.js +++ b/packages/worker/src/api/controllers/global/users.js @@ -1,8 +1,7 @@ const { generateGlobalUserID, getGlobalUserParams, - getGlobalDB, - getGlobalDBFromCtx, + StaticDatabases, } = require("@budibase/auth/db") const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils @@ -14,6 +13,7 @@ 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 PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants @@ -73,8 +73,8 @@ async function doesTenantExist(tenantId) { ) } -async function allUsers(ctx) { - const db = getGlobalDBFromCtx(ctx) +async function allUsers() { + const db = getGlobalDB() const response = await db.allDocs( getGlobalUserParams(null, { include_docs: true, @@ -87,12 +87,13 @@ async function saveUser(user, tenantId) { if (!tenantId) { throw "No tenancy specified." } + // specify the tenancy incase we're making a new admin user (public) const db = getGlobalDB(tenantId) let { email, password, _id } = user // make sure another user isn't using the same email let dbUser if (email) { - dbUser = await getGlobalUserByEmail(email, tenantId) + dbUser = await getGlobalUserByEmail(email) if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { throw "Email address already in use." } @@ -148,10 +149,8 @@ async function saveUser(user, tenantId) { } exports.save = async ctx => { - // this always stores the user into the requesting users tenancy - const tenantId = ctx.user.tenantId try { - ctx.body = await saveUser(ctx.request.body, tenantId) + ctx.body = await saveUser(ctx.request.body, getTenantId()) } catch (err) { ctx.throw(err.status || 400, err) } @@ -163,7 +162,7 @@ exports.adminUser = async ctx => { ctx.throw(403, "Organisation already exists.") } - const db = getGlobalDB(tenantId) + const db = getGlobalDB() const response = await db.allDocs( getGlobalUserParams(null, { include_docs: true, @@ -197,7 +196,7 @@ exports.adminUser = async ctx => { } exports.destroy = async ctx => { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const dbUser = await db.get(ctx.params.id) await db.remove(dbUser._id, dbUser._rev) await userCache.invalidateUser(dbUser._id) @@ -209,7 +208,7 @@ exports.destroy = async ctx => { exports.removeAppRole = async ctx => { const { appId } = ctx.params - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const users = await allUsers(ctx) const bulk = [] const cacheInvalidations = [] @@ -239,7 +238,7 @@ exports.getSelf = async ctx => { } exports.updateSelf = async ctx => { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const user = await db.get(ctx.user._id) if (ctx.request.body.password) { ctx.request.body.password = await hash(ctx.request.body.password) @@ -272,7 +271,7 @@ exports.fetch = async ctx => { // called internally by app server user find exports.find = async ctx => { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() let user try { user = await db.get(ctx.params.id) @@ -310,16 +309,14 @@ exports.tenantLookup = async ctx => { exports.invite = async ctx => { let { email, userInfo } = ctx.request.body - const tenantId = ctx.user.tenantId - const existing = await getGlobalUserByEmail(email, tenantId) + const existing = await getGlobalUserByEmail(email) if (existing) { ctx.throw(400, "Email address already in use.") } if (!userInfo) { userInfo = {} } - userInfo.tenantId = tenantId - await sendEmail(tenantId, email, EmailTemplatePurpose.INVITATION, { + await sendEmail(email, EmailTemplatePurpose.INVITATION, { subject: "{{ company }} platform invitation", info: userInfo, }) @@ -333,17 +330,13 @@ exports.inviteAccept = async ctx => { try { // info is an extension of the user object that was stored by global const { email, info } = await checkInviteCode(inviteCode) - // only pass through certain props for accepting - ctx.request.body = { + ctx.body = await saveUser({ firstName, lastName, password, email, ...info, - } - ctx.user = { - tenantId: info.tenantId, - } + }, info.tenantId) // this will flesh out the body response await exports.save(ctx) } catch (err) { diff --git a/packages/worker/src/api/controllers/global/workspaces.js b/packages/worker/src/api/controllers/global/workspaces.js index e2910a2364..fb1a859826 100644 --- a/packages/worker/src/api/controllers/global/workspaces.js +++ b/packages/worker/src/api/controllers/global/workspaces.js @@ -1,11 +1,11 @@ const { getWorkspaceParams, generateWorkspaceID, - getGlobalDBFromCtx, } = require("@budibase/auth/db") +const { getGlobalDB } = require("@budibase/auth/tenancy") exports.save = async function (ctx) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const workspaceDoc = ctx.request.body // workspace does not exist yet @@ -25,7 +25,7 @@ exports.save = async function (ctx) { } exports.fetch = async function (ctx) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const response = await db.allDocs( getWorkspaceParams(undefined, { include_docs: true, @@ -35,7 +35,7 @@ exports.fetch = async function (ctx) { } exports.find = async function (ctx) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() try { ctx.body = await db.get(ctx.params.id) } catch (err) { @@ -44,7 +44,7 @@ exports.find = async function (ctx) { } exports.destroy = async function (ctx) { - const db = getGlobalDBFromCtx(ctx) + const db = getGlobalDB() const { id, rev } = ctx.params try { diff --git a/packages/worker/src/api/index.js b/packages/worker/src/api/index.js index 2e65dc17e7..d39546ff3a 100644 --- a/packages/worker/src/api/index.js +++ b/packages/worker/src/api/index.js @@ -2,7 +2,18 @@ const Router = require("@koa/router") const compress = require("koa-compress") const zlib = require("zlib") const { routes } = require("./routes") -const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth +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 = [ { @@ -53,6 +64,7 @@ router }) ) .use("/health", ctx => (ctx.status = 200)) + .use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS)) .use(buildAuthMiddleware(PUBLIC_ENDPOINTS)) // for now no public access is allowed to worker (bar health check) .use((ctx, next) => { diff --git a/packages/worker/src/constants/templates/index.js b/packages/worker/src/constants/templates/index.js index 805e4b79b7..44fe345af1 100644 --- a/packages/worker/src/constants/templates/index.js +++ b/packages/worker/src/constants/templates/index.js @@ -8,9 +8,8 @@ const { const { join } = require("path") const { getTemplateParams, - getTenantIdFromCtx, - getGlobalDB, } = require("@budibase/auth/db") +const { getGlobalDB } = require("@budibase/auth/tenancy") exports.EmailTemplates = { [EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile( @@ -52,13 +51,8 @@ exports.addBaseTemplates = (templates, type = null) => { return templates } -exports.getTemplatesCtx = async (ctx, opts = {}) => { - const tenantId = getTenantIdFromCtx(ctx) - return exports.getTemplates(tenantId, opts) -} - -exports.getTemplates = async (tenantId, { ownerId, type, id } = {}) => { - const db = getGlobalDB(tenantId) +exports.getTemplates = async ({ ownerId, type, id } = {}) => { + const db = getGlobalDB() const response = await db.allDocs( getTemplateParams(ownerId || GLOBAL_OWNER, id, { include_docs: true, @@ -75,10 +69,7 @@ exports.getTemplates = async (tenantId, { ownerId, type, id } = {}) => { return exports.addBaseTemplates(templates, type) } -exports.getTemplateByPurpose = async ({ tenantId, ctx }, type, purpose) => { - if (!tenantId && ctx) { - tenantId = getTenantIdFromCtx(ctx) - } - const templates = await exports.getTemplates(tenantId, { type }) +exports.getTemplateByPurpose = async (type, purpose) => { + const templates = await exports.getTemplates({ type }) return templates.find(template => template.purpose === purpose) } diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js index eec2743b69..22de6b4d6d 100644 --- a/packages/worker/src/utilities/email.js +++ b/packages/worker/src/utilities/email.js @@ -1,10 +1,11 @@ const nodemailer = require("nodemailer") -const { getGlobalDB, getScopedConfig } = require("@budibase/auth/db") +const { getScopedConfig } = require("@budibase/auth/db") const { EmailTemplatePurpose, TemplateTypes, Configs } = require("../constants") const { getTemplateByPurpose } = require("../constants/templates") const { getSettingsTemplateContext } = require("./templates") const { processString } = require("@budibase/string-templates") const { getResetPasswordCode, getInviteCode } = require("../utilities/redis") +const { getGlobalDB } = require("@budibase/auth/tenancy") const TEST_MODE = false const TYPE = TemplateTypes.EMAIL @@ -60,7 +61,6 @@ async function getLinkCode(purpose, email, user, info = null) { /** * Builds an email using handlebars and the templates found in the system (default or otherwise). - * @param {string} tenantId the ID of the tenant which is sending the email. * @param {string} purpose the purpose of the email being built, e.g. invitation, password reset. * @param {string} email the address which it is being sent to for contextual purposes. * @param {object} context the context which is being used for building the email (hbs context). @@ -69,7 +69,6 @@ async function getLinkCode(purpose, email, user, info = null) { * @return {Promise} returns the built email HTML if all provided parameters were valid. */ async function buildEmail( - tenantId, purpose, email, context, @@ -80,8 +79,8 @@ async function buildEmail( throw `Unable to build an email of type ${purpose}` } let [base, body] = await Promise.all([ - getTemplateByPurpose({ tenantId }, TYPE, EmailTemplatePurpose.BASE), - getTemplateByPurpose({ tenantId }, TYPE, purpose), + getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE), + getTemplateByPurpose(TYPE, purpose), ]) if (!base || !body) { throw "Unable to build email, missing base components" @@ -123,12 +122,12 @@ async function getSmtpConfiguration(db, workspaceId = null) { * Checks if a SMTP config exists based on passed in parameters. * @return {Promise} returns true if there is a configuration that can be used. */ -exports.isEmailConfigured = async (tenantId, workspaceId = null) => { +exports.isEmailConfigured = async (workspaceId = null) => { // when "testing" simply return true if (TEST_MODE) { return true } - const db = getGlobalDB(tenantId) + const db = getGlobalDB() const config = await getSmtpConfiguration(db, workspaceId) return config != null } @@ -136,7 +135,6 @@ exports.isEmailConfigured = async (tenantId, workspaceId = null) => { /** * Given an email address and an email purpose this will retrieve the SMTP configuration and * send an email using it. - * @param {string} tenantId The tenant which is sending them email. * @param {string} email The email address to send to. * @param {string} purpose The purpose of the email being sent (e.g. reset password). * @param {string|undefined} workspaceId If finer grain controls being used then this will lookup config for workspace. @@ -149,12 +147,11 @@ exports.isEmailConfigured = async (tenantId, workspaceId = null) => { * nodemailer response. */ exports.sendEmail = async ( - tenantId, email, purpose, { workspaceId, user, from, contents, subject, info } = {} ) => { - const db = getGlobalDB(tenantId) + const db = getGlobalDB() let config = (await getSmtpConfiguration(db, workspaceId)) || {} if (Object.keys(config).length === 0 && !TEST_MODE) { throw "Unable to find SMTP configuration." @@ -162,11 +159,11 @@ exports.sendEmail = async ( const transport = createSMTPTransport(config) // if there is a link code needed this will retrieve it const code = await getLinkCode(purpose, email, user, info) - const context = await getSettingsTemplateContext(tenantId, purpose, code) + const context = await getSettingsTemplateContext(purpose, code) const message = { from: from || config.from, to: email, - html: await buildEmail(tenantId, purpose, email, context, { + html: await buildEmail(purpose, email, context, { user, contents, }), diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.js index 51628064bb..31d92e9226 100644 --- a/packages/worker/src/utilities/templates.js +++ b/packages/worker/src/utilities/templates.js @@ -1,4 +1,4 @@ -const { getScopedConfig, getGlobalDB } = require("@budibase/auth/db") +const { getScopedConfig } = require("@budibase/auth/db") const { Configs, InternalTemplateBindings, @@ -7,20 +7,13 @@ const { } = require("../constants") const { checkSlashesInUrl } = require("./index") const env = require("../environment") +const { getGlobalDB, addTenantToUrl } = require("@budibase/auth/tenancy") const LOCAL_URL = `http://localhost:${env.CLUSTER_PORT || 10000}` const BASE_COMPANY = "Budibase" -function addTenantToUrl(url, tenantId) { - if (env.MULTI_TENANCY) { - const char = url.indexOf("?") === -1 ? "?" : "&" - url += `${char}tenantId=${tenantId}` - } - return url -} - -exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => { - const db = getGlobalDB(tenantId) +exports.getSettingsTemplateContext = async (purpose, code = null) => { + const db = getGlobalDB() // TODO: use more granular settings in the future if required let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {} if (!settings || !settings.platformUrl) { @@ -35,7 +28,7 @@ exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => { [InternalTemplateBindings.DOCS_URL]: settings.docsUrl || "https://docs.budibase.com/", [InternalTemplateBindings.LOGIN_URL]: checkSlashesInUrl( - addTenantToUrl(`${URL}/login`, tenantId) + addTenantToUrl(`${URL}/login`) ), [InternalTemplateBindings.CURRENT_DATE]: new Date().toISOString(), [InternalTemplateBindings.CURRENT_YEAR]: new Date().getFullYear(), @@ -45,15 +38,14 @@ exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => { case EmailTemplatePurpose.PASSWORD_RECOVERY: context[InternalTemplateBindings.RESET_CODE] = code context[InternalTemplateBindings.RESET_URL] = checkSlashesInUrl( - addTenantToUrl(`${URL}/builder/auth/reset?code=${code}`, tenantId) + addTenantToUrl(`${URL}/builder/auth/reset?code=${code}`) ) break case EmailTemplatePurpose.INVITATION: context[InternalTemplateBindings.INVITE_CODE] = code context[InternalTemplateBindings.INVITE_URL] = checkSlashesInUrl( addTenantToUrl( - `${URL}/builder/invite?code=${code}&tenantId=${tenantId}`, - tenantId + `${URL}/builder/invite?code=${code}` ) ) break