mirror of https://github.com/Budibase/budibase.git
32 changed files with 461 additions and 258 deletions
@ -1 +1,4 @@ |
|||
module.exports = require("./src/db/utils") |
|||
module.exports = { |
|||
...require("./src/db/utils"), |
|||
...require("./src/db/constants"), |
|||
} |
|||
|
|||
@ -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", |
|||
}, |
|||
}, |
|||
} |
|||
@ -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 |
|||
}) |
|||
} |
|||
@ -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() |
|||
}) |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
module.exports = { |
|||
...require("./context"), |
|||
...require("./tenancy"), |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -0,0 +1 @@ |
|||
module.exports = require("./src/tenancy") |
|||
Loading…
Reference in new issue