budibase/packages/backend-core/src/tenancy/tenancy.ts

149 lines
3.5 KiB
TypeScript

import {
DEFAULT_TENANT_ID,
getTenantId,
getTenantIDFromAppID,
isMultiTenant,
getPlatformURL,
} from "../context"
import {
Ctx,
TenantResolutionStrategy,
GetTenantIdOptions,
} from "@budibase/types"
import { Header } from "../constants"
export function addTenantToUrl(url: string) {
const tenantId = getTenantId()
if (isMultiTenant()) {
const char = url.indexOf("?") === -1 ? "?" : "&"
url += `${char}tenantId=${tenantId}`
}
return url
}
export const isUserInAppTenant = (appId: string, user?: any) => {
let userTenantId
if (user) {
userTenantId = user.tenantId || DEFAULT_TENANT_ID
} else {
userTenantId = getTenantId()
}
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
return tenantId === userTenantId
}
const ALL_STRATEGIES = Object.values(TenantResolutionStrategy)
export const getTenantIDFromCtx = (
ctx: Ctx,
opts: GetTenantIdOptions
): string | undefined => {
// exit early if not multi-tenant
if (!isMultiTenant()) {
return DEFAULT_TENANT_ID
}
// opt defaults
if (opts.allowNoTenant === undefined) {
opts.allowNoTenant = false
}
if (!opts.includeStrategies) {
opts.includeStrategies = ALL_STRATEGIES
}
if (!opts.excludeStrategies) {
opts.excludeStrategies = []
}
const isAllowed = (strategy: TenantResolutionStrategy) => {
// excluded takes precedence
if (opts.excludeStrategies?.includes(strategy)) {
return false
}
if (opts.includeStrategies?.includes(strategy)) {
return true
}
}
// always use user first
if (isAllowed(TenantResolutionStrategy.USER)) {
const userTenantId = ctx.user?.tenantId
if (userTenantId) {
return userTenantId
}
}
// header
if (isAllowed(TenantResolutionStrategy.HEADER)) {
const headerTenantId = ctx.request.headers[Header.TENANT_ID]
if (headerTenantId) {
return headerTenantId as string
}
}
// query param
if (isAllowed(TenantResolutionStrategy.QUERY)) {
const queryTenantId = ctx.request.query.tenantId
if (queryTenantId) {
return queryTenantId as string
}
}
// subdomain
if (isAllowed(TenantResolutionStrategy.SUBDOMAIN)) {
// e.g. budibase.app or local.com:10000
let platformHost
try {
platformHost = new URL(getPlatformURL()).host.split(":")[0]
} catch (err: any) {
// if invalid URL, just don't try to process subdomain
if (err.code !== "ERR_INVALID_URL") {
throw err
}
}
// e.g. tenant.budibase.app or tenant.local.com
const requestHost = ctx.host
// parse the tenant id from the difference
if (platformHost && requestHost.includes(platformHost)) {
const tenantId = requestHost.substring(
0,
requestHost.indexOf(`.${platformHost}`)
)
if (tenantId) {
return tenantId
}
}
}
// path
if (isAllowed(TenantResolutionStrategy.PATH)) {
// params - have to parse manually due to koa-router not run yet
const match = ctx.matched.find(
(m: any) => !!m.paramNames.find((p: any) => p.name === "tenantId")
)
// get the raw path url - without any query params
const ctxUrl = ctx.originalUrl
let url
if (ctxUrl.includes("?")) {
url = ctxUrl.split("?")[0]
} else {
url = ctxUrl
}
if (match) {
const params = match.params(url, match.captures(url), {})
if (params.tenantId) {
return params.tenantId
}
}
}
if (!opts.allowNoTenant) {
ctx.throw(403, "Tenant id not set")
}
return undefined
}