diff --git a/packages/backend-core/src/context/constants.ts b/packages/backend-core/src/context/constants.ts index 14ab9a531c..eb156c357b 100644 --- a/packages/backend-core/src/context/constants.ts +++ b/packages/backend-core/src/context/constants.ts @@ -1,9 +1,11 @@ +import { IdentityContext } from "@budibase/types" + export enum ContextKey { MAIN = "main", } -export enum ContextElement { - TENANT_ID = "tenantId", - APP_ID = "appId", - IDENTITY = "identity", +export type ContextMap = { + tenantId?: string + appId?: string + identity?: IdentityContext } diff --git a/packages/backend-core/src/context/index.ts b/packages/backend-core/src/context/index.ts index f88fe012a9..e8813c5a2f 100644 --- a/packages/backend-core/src/context/index.ts +++ b/packages/backend-core/src/context/index.ts @@ -4,12 +4,10 @@ import cls from "./FunctionContext" import { baseGlobalDBName } from "../db/tenancy" import { IdentityContext } from "@budibase/types" import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants" -import { ContextElement, ContextKey } from "./constants" +import { ContextMap, ContextKey } from "./constants" import { PouchLike } from "../couch" import { getDevelopmentAppID, getProdAppID } from "../db/conversions" -type ContextMap = { [key in ContextElement]?: any } - export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID // some test cases call functions directly, need to @@ -22,7 +20,7 @@ export function isMultiTenant() { export function isTenantIdSet() { const context = cls.getFromContext(ContextKey.MAIN) as ContextMap - return !!context?.[ContextElement.TENANT_ID] + return !!context?.tenantId } export function isTenancyEnabled() { @@ -35,7 +33,7 @@ export function isTenancyEnabled() { */ export function getTenantIDFromAppID(appId: string) { if (!appId) { - return null + return undefined } if (!isMultiTenant()) { return DEFAULT_TENANT_ID @@ -43,7 +41,7 @@ export function getTenantIDFromAppID(appId: string) { const split = appId.split(SEPARATOR) const hasDev = split[1] === DocumentType.DEV if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { - return null + return undefined } if (hasDev) { return split[2] @@ -80,8 +78,8 @@ export async function doInContext(appId: string, task: any): Promise { const tenantId = getTenantIDFromAppID(appId) return newContext( { - [ContextElement.TENANT_ID]: tenantId, - [ContextElement.APP_ID]: appId, + tenantId, + appId, }, task ) @@ -96,12 +94,8 @@ export async function doInTenant( tenantId = tenantId || DEFAULT_TENANT_ID } - return newContext( - { - [ContextElement.TENANT_ID]: tenantId, - }, - task - ) + const updates = tenantId ? { tenantId } : {} + return newContext(updates, task) } export async function doInAppContext(appId: string, task: any): Promise { @@ -112,8 +106,8 @@ export async function doInAppContext(appId: string, task: any): Promise { const tenantId = getTenantIDFromAppID(appId) return newContext( { - [ContextElement.TENANT_ID]: tenantId, - [ContextElement.APP_ID]: appId, + tenantId, + appId, }, task ) @@ -128,10 +122,10 @@ export async function doInIdentityContext( } const context: ContextMap = { - [ContextElement.IDENTITY]: identity, + identity, } if (identity.tenantId) { - context[ContextElement.TENANT_ID] = identity.tenantId + context.tenantId = identity.tenantId } return newContext(context, task) } @@ -139,7 +133,7 @@ export async function doInIdentityContext( export function getIdentity(): IdentityContext | undefined { try { const context = cls.getFromContext(ContextKey.MAIN) as ContextMap - return context?.[ContextElement.IDENTITY] + return context?.identity } catch (e) { // do nothing - identity is not in context } @@ -150,7 +144,7 @@ export function getTenantId(): string { return DEFAULT_TENANT_ID } const context = cls.getFromContext(ContextKey.MAIN) as ContextMap - const tenantId = context?.[ContextElement.TENANT_ID] + const tenantId = context?.tenantId if (!tenantId) { throw new Error("Tenant id not found") } @@ -159,7 +153,7 @@ export function getTenantId(): string { export function getAppId(): string | undefined { const context = cls.getFromContext(ContextKey.MAIN) as ContextMap - const foundId = context?.[ContextElement.APP_ID] + const foundId = context?.appId if (!foundId && env.isTest() && TEST_APP_ID) { return TEST_APP_ID } else { @@ -167,16 +161,16 @@ export function getAppId(): string | undefined { } } -export function updateTenantId(tenantId: string | null) { +export function updateTenantId(tenantId?: string) { let context: ContextMap = updateContext({ - [ContextElement.TENANT_ID]: tenantId, + tenantId, }) cls.setOnContext(ContextKey.MAIN, context) } export function updateAppId(appId: string) { let context: ContextMap = updateContext({ - [ContextElement.APP_ID]: appId, + appId, }) try { cls.setOnContext(ContextKey.MAIN, context) @@ -191,7 +185,7 @@ export function updateAppId(appId: string) { export function getGlobalDB(): PouchLike { const context = cls.getFromContext(ContextKey.MAIN) as ContextMap - return new PouchLike(baseGlobalDBName(context?.[ContextElement.TENANT_ID])) + return new PouchLike(baseGlobalDBName(context?.tenantId)) } /** diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index 1064936fd7..cba88d9751 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -147,9 +147,9 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string) { * @param {string|null} roleId The level ID to lookup. * @returns {Promise} The role object, which may contain an "inherits" property. */ -export async function getRole(roleId?: string) { +export async function getRole(roleId?: string): Promise { if (!roleId) { - return null + return undefined } let role: any = {} // built in roles mostly come from the in-code implementation, @@ -193,7 +193,9 @@ async function getAllUserRoles(userRoleId?: string): Promise { ) { roleIds.push(currentRole.inherits) currentRole = await getRole(currentRole.inherits) - roles.push(currentRole) + if (currentRole) { + roles.push(currentRole) + } } return roles } diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index ad5c6b5287..55ac66b95c 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -3,10 +3,10 @@ import { queryPlatformView } from "../db/views" import { StaticDatabases, ViewName } from "../db/constants" import { getGlobalDBName } from "../db/tenancy" import { - getTenantId, DEFAULT_TENANT_ID, - isMultiTenant, + getTenantId, getTenantIDFromAppID, + isMultiTenant, } from "../context" import env from "../environment" import { PlatformUser } from "@budibase/types" @@ -14,7 +14,7 @@ import { PlatformUser } from "@budibase/types" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name -export const addTenantToUrl = (url: string) => { +export function addTenantToUrl(url: string) { const tenantId = getTenantId() if (isMultiTenant()) { @@ -25,7 +25,7 @@ export const addTenantToUrl = (url: string) => { return url } -export const doesTenantExist = async (tenantId: string) => { +export async function doesTenantExist(tenantId: string) { return doWithDB(PLATFORM_INFO_DB, async (db: any) => { let tenants try { @@ -42,12 +42,12 @@ export const doesTenantExist = async (tenantId: string) => { }) } -export const tryAddTenant = async ( +export async function tryAddTenant( tenantId: string, userId: string, email: string, afterCreateTenant: () => Promise -) => { +) { return doWithDB(PLATFORM_INFO_DB, async (db: any) => { const getDoc = async (id: string) => { if (!id) { @@ -89,11 +89,11 @@ export const tryAddTenant = async ( }) } -export const doWithGlobalDB = (tenantId: string, cb: any) => { +export function doWithGlobalDB(tenantId: string, cb: any) { return doWithDB(getGlobalDBName(tenantId), cb) } -export const lookupTenantId = async (userId: string) => { +export async function lookupTenantId(userId: string) { return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null try { @@ -109,19 +109,26 @@ export const lookupTenantId = async (userId: string) => { } // lookup, could be email or userId, either will return a doc -export const getTenantUser = async ( +export async function getTenantUser( identifier: string -): Promise => { +): Promise { // use the view here and allow to find anyone regardless of casing - // Use lowercase to ensure email login is case insensitive - const response = queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { - keys: [identifier.toLowerCase()], - include_docs: true, - }) as Promise - return response + // Use lowercase to ensure email login is case-insensitive + const users = await queryPlatformView( + ViewName.PLATFORM_USERS_LOWERCASE, + { + keys: [identifier.toLowerCase()], + include_docs: true, + } + ) + if (Array.isArray(users)) { + return users[0] + } else { + return users + } } -export const isUserInAppTenant = (appId: string, user?: any) => { +export function isUserInAppTenant(appId: string, user?: any) { let userTenantId if (user) { userTenantId = user.tenantId || DEFAULT_TENANT_ID @@ -132,7 +139,7 @@ export const isUserInAppTenant = (appId: string, user?: any) => { return tenantId === userTenantId } -export const getTenantIds = async () => { +export async function getTenantIds() { return doWithDB(PLATFORM_INFO_DB, async (db: any) => { let tenants try { diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.ts similarity index 58% rename from packages/server/src/middleware/currentapp.js rename to packages/server/src/middleware/currentapp.ts index f9c4ae40db..9496ddd0a0 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.ts @@ -1,29 +1,26 @@ -const { - getAppIdFromCtx, - setCookie, - getCookie, - clearCookie, -} = require("@budibase/backend-core/utils") -const { Cookies, Headers } = require("@budibase/backend-core/constants") -const { getRole } = require("@budibase/backend-core/roles") -const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles") -const { generateUserMetadataID, isDevAppID } = require("../db/utils") -const { dbExists } = require("@budibase/backend-core/db") -const { isUserInAppTenant } = require("@budibase/backend-core/tenancy") -const { getCachedSelf } = require("../utilities/global") -const env = require("../environment") -const { isWebhookEndpoint } = require("./utils") -const { doInAppContext } = require("@budibase/backend-core/context") +import { + utils, + constants, + roles, + db as dbCore, + tenancy, + context, +} from "@budibase/backend-core" +import { generateUserMetadataID, isDevAppID } from "../db/utils" +import { getCachedSelf } from "../utilities/global" +import env from "../environment" +import { isWebhookEndpoint } from "./utils" +import { BBContext } from "@budibase/types" -module.exports = async (ctx, next) => { +export = async (ctx: BBContext, next: any) => { // try to get the appID from the request - let requestAppId = await getAppIdFromCtx(ctx) + let requestAppId = await utils.getAppIdFromCtx(ctx) // get app cookie if it exists - let appCookie = null + let appCookie: { appId?: string } | undefined try { - appCookie = getCookie(ctx, Cookies.CurrentApp) + appCookie = utils.getCookie(ctx, constants.Cookies.CurrentApp) } catch (err) { - clearCookie(ctx, Cookies.CurrentApp) + utils.clearCookie(ctx, constants.Cookies.CurrentApp) } if (!appCookie && !requestAppId) { return next() @@ -31,9 +28,9 @@ module.exports = async (ctx, next) => { // check the app exists referenced in cookie if (appCookie) { const appId = appCookie.appId - const exists = await dbExists(appId) + const exists = await dbCore.dbExists(appId) if (!exists) { - clearCookie(ctx, Cookies.CurrentApp) + utils.clearCookie(ctx, constants.Cookies.CurrentApp) return next() } // if the request app ID wasn't set, update it with the cookie @@ -47,13 +44,13 @@ module.exports = async (ctx, next) => { !isWebhookEndpoint(ctx) && (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) ) { - clearCookie(ctx, Cookies.CurrentApp) + utils.clearCookie(ctx, constants.Cookies.CurrentApp) return ctx.redirect("/") } } - let appId, - roleId = BUILTIN_ROLE_IDS.PUBLIC + let appId: string | undefined, + roleId = roles.BUILTIN_ROLE_IDS.PUBLIC if (!ctx.user) { // not logged in, try to set a cookie for public apps appId = requestAppId @@ -68,16 +65,20 @@ module.exports = async (ctx, next) => { const isBuilder = globalUser && globalUser.builder && globalUser.builder.global const isDevApp = appId && isDevAppID(appId) - const roleHeader = ctx.request && ctx.request.headers[Headers.PREVIEW_ROLE] + const roleHeader = + ctx.request && + (ctx.request.headers[constants.Headers.PREVIEW_ROLE] as string) if (isBuilder && isDevApp && roleHeader) { // Ensure the role is valid by ensuring a definition exists try { - await getRole(roleHeader) - roleId = roleHeader + if (roleHeader) { + await roles.getRole(roleHeader) + roleId = roleHeader - // Delete admin and builder flags so that the specified role is honoured - delete ctx.user.builder - delete ctx.user.admin + // Delete admin and builder flags so that the specified role is honoured + delete ctx.user.builder + delete ctx.user.admin + } } catch (error) { // Swallow error and do nothing } @@ -89,21 +90,22 @@ module.exports = async (ctx, next) => { return next() } - return doInAppContext(appId, async () => { + return context.doInAppContext(appId, async () => { let skipCookie = false // if the user not in the right tenant then make sure they have no permissions // need to judge this only based on the request app ID, if ( env.MULTI_TENANCY && - ctx.user & requestAppId && - !isUserInAppTenant(requestAppId, ctx.user) + ctx.user && + requestAppId && + !tenancy.isUserInAppTenant(requestAppId, ctx.user) ) { // don't error, simply remove the users rights (they are a public user) delete ctx.user.builder delete ctx.user.admin delete ctx.user.roles ctx.isAuthenticated = false - roleId = BUILTIN_ROLE_IDS.PUBLIC + roleId = roles.BUILTIN_ROLE_IDS.PUBLIC skipCookie = true } @@ -111,15 +113,17 @@ module.exports = async (ctx, next) => { if (roleId) { ctx.roleId = roleId const globalId = ctx.user ? ctx.user._id : undefined - const userId = ctx.user ? generateUserMetadataID(ctx.user._id) : null + const userId = ctx.user + ? generateUserMetadataID(ctx.user._id!) + : undefined ctx.user = { - ...ctx.user, + ...ctx.user!, // override userID with metadata one _id: userId, userId, globalId, roleId, - role: await getRole(roleId), + role: await roles.getRole(roleId), } } if ( @@ -128,7 +132,7 @@ module.exports = async (ctx, next) => { appCookie.appId !== requestAppId) && !skipCookie ) { - setCookie(ctx, { appId }, Cookies.CurrentApp) + utils.setCookie(ctx, { appId }, constants.Cookies.CurrentApp) } return next() diff --git a/packages/server/src/middleware/tests/currentapp.spec.js b/packages/server/src/middleware/tests/currentapp.spec.js index 57c21b2107..638abcbba4 100644 --- a/packages/server/src/middleware/tests/currentapp.spec.js +++ b/packages/server/src/middleware/tests/currentapp.spec.js @@ -7,7 +7,7 @@ jest.mock("@budibase/backend-core/db", () => { coreDb.init() return { ...coreDb, - dbExists: () => true, + dbExists: async () => true, } }) diff --git a/packages/types/src/sdk/koa.ts b/packages/types/src/sdk/koa.ts index 8004ba72ae..b8831dcbee 100644 --- a/packages/types/src/sdk/koa.ts +++ b/packages/types/src/sdk/koa.ts @@ -1,10 +1,14 @@ import { Context, Request } from "koa" -import { User } from "../documents" +import { User, Role, UserRoles } from "../documents" import { License } from "../sdk" -export interface ContextUser extends User { +export interface ContextUser extends Omit { globalId?: string license: License + userId?: string + roleId?: string + role?: Role + roles?: UserRoles } export interface BBRequest extends Request {