From 5cd6cb166ab51f0d07bab1367d0549abd8aaa6af Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 11:53:01 +0000 Subject: [PATCH 01/12] Configurable test log levels and common error handling --- .../src/middleware/errorHandling.ts | 28 +++++++++++++++ packages/backend-core/src/middleware/index.ts | 5 +-- packages/backend-core/tests/jestEnv.ts | 28 +++------------ packages/backend-core/tests/jestSetup.ts | 21 +++++++++++- packages/backend-core/tests/logging.ts | 34 +++++++++++++++++++ packages/server/src/api/index.ts | 25 ++------------ packages/server/src/tests/jestEnv.ts | 12 +++---- packages/server/src/tests/jestSetup.ts | 1 + packages/server/src/tests/logging.ts | 34 +++++++++++++++++++ packages/types/src/api/web/errors.ts | 1 + packages/worker/src/api/index.ts | 24 +++---------- .../routes/system/tests/migrations.spec.ts | 4 +-- .../src/middleware/tests/tenancy.spec.ts | 8 ++--- packages/worker/src/tests/jestEnv.ts | 2 +- packages/worker/src/tests/jestSetup.ts | 7 ++-- packages/worker/src/tests/logging.ts | 34 +++++++++++++++++++ 16 files changed, 182 insertions(+), 86 deletions(-) create mode 100644 packages/backend-core/src/middleware/errorHandling.ts create mode 100644 packages/backend-core/tests/logging.ts create mode 100644 packages/server/src/tests/logging.ts create mode 100644 packages/worker/src/tests/logging.ts diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts new file mode 100644 index 0000000000..1baaa92501 --- /dev/null +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -0,0 +1,28 @@ +import { APIError } from "@budibase/types" +import * as errors from "../errors" +import env from "../environment" + +export async function errorHandling(ctx: any, next: any) { + try { + await next() + } catch (err: any) { + const status = err.status || err.statusCode || 500 + ctx.status = status + + if (status > 499 || env.LOG_4XX) { + ctx.log.error(err) + } + + const error = errors.getPublicError(err) + const body: APIError = { + message: err.message, + status: status, + validationErrors: err.validation, + error, + } + + ctx.body = body + } +} + +export default errorHandling diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index 4986cde64b..de609f9a3e 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -1,7 +1,7 @@ export * as jwt from "./passport/jwt" export * as local from "./passport/local" -export * as google from "./passport/google" -export * as oidc from "./passport/oidc" +export * as google from "./passport/sso/google" +export * as oidc from "./passport/sso/oidc" import * as datasourceGoogle from "./passport/datasource/google" export const datasource = { google: datasourceGoogle, @@ -16,4 +16,5 @@ export { default as adminOnly } from "./adminOnly" export { default as builderOrAdmin } from "./builderOrAdmin" export { default as builderOnly } from "./builderOnly" export { default as logging } from "./logging" +export { default as errorHandling } from "./errorHandling" export * as joiValidator from "./joi-validator" diff --git a/packages/backend-core/tests/jestEnv.ts b/packages/backend-core/tests/jestEnv.ts index 1190eb3bb7..71cf865737 100644 --- a/packages/backend-core/tests/jestEnv.ts +++ b/packages/backend-core/tests/jestEnv.ts @@ -1,23 +1,5 @@ -import env from "../src/environment" -import { mocks } from "./utilities" - -// must explicitly enable fetch mock -mocks.fetch.enable() - -// mock all dates to 2020-01-01T00:00:00.000Z -// use tk.reset() to use real dates in individual tests -import tk from "timekeeper" -tk.freeze(mocks.date.MOCK_DATE) - -env._set("SELF_HOSTED", "1") -env._set("NODE_ENV", "jest") - -if (!process.env.DEBUG) { - global.console.log = jest.fn() // console.log are ignored in tests -} - -if (!process.env.CI) { - // set a longer timeout in dev for debugging - // 100 seconds - jest.setTimeout(100000) -} +process.env.SELF_HOSTED = "1" +process.env.MULTI_TENANCY = "1" +process.env.NODE_ENV = "jest" +process.env.MOCK_REDIS = "1" +process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" diff --git a/packages/backend-core/tests/jestSetup.ts b/packages/backend-core/tests/jestSetup.ts index f7887ec824..e786086de6 100644 --- a/packages/backend-core/tests/jestSetup.ts +++ b/packages/backend-core/tests/jestSetup.ts @@ -1,4 +1,23 @@ +import "./logging" import env from "../src/environment" -import { testContainerUtils } from "./utilities" +import { mocks, testContainerUtils } from "./utilities" + +// must explicitly enable fetch mock +mocks.fetch.enable() + +// mock all dates to 2020-01-01T00:00:00.000Z +// use tk.reset() to use real dates in individual tests +import tk from "timekeeper" +tk.freeze(mocks.date.MOCK_DATE) + +if (!process.env.DEBUG) { + console.log = jest.fn() // console.log are ignored in tests +} + +if (!process.env.CI) { + // set a longer timeout in dev for debugging + // 100 seconds + jest.setTimeout(100000) +} testContainerUtils.setupEnv(env) diff --git a/packages/backend-core/tests/logging.ts b/packages/backend-core/tests/logging.ts new file mode 100644 index 0000000000..271f4d62ff --- /dev/null +++ b/packages/backend-core/tests/logging.ts @@ -0,0 +1,34 @@ +export enum LogLevel { + TRACE = "trace", + DEBUG = "debug", + INFO = "info", + WARN = "warn", + ERROR = "error", +} + +const LOG_INDEX: { [key in LogLevel]: number } = { + [LogLevel.TRACE]: 1, + [LogLevel.DEBUG]: 2, + [LogLevel.INFO]: 3, + [LogLevel.WARN]: 4, + [LogLevel.ERROR]: 5, +} + +const setIndex = LOG_INDEX[process.env.LOG_LEVEL as LogLevel] + +if (setIndex > LOG_INDEX.trace) { + global.console.trace = jest.fn() +} + +if (setIndex > LOG_INDEX.debug) { + global.console.debug = jest.fn() +} + +if (setIndex > LOG_INDEX.info) { + global.console.info = jest.fn() + global.console.log = jest.fn() +} + +if (setIndex > LOG_INDEX.warn) { + global.console.warn = jest.fn() +} diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts index 3375161dd8..78a6056366 100644 --- a/packages/server/src/api/index.ts +++ b/packages/server/src/api/index.ts @@ -1,5 +1,5 @@ import Router from "@koa/router" -import { errors, auth } from "@budibase/backend-core" +import { auth, middleware } from "@budibase/backend-core" import currentApp from "../middleware/currentapp" import zlib from "zlib" import { mainRoutes, staticRoutes, publicRoutes } from "./routes" @@ -14,6 +14,8 @@ export const router: Router = new Router() router.get("/health", ctx => (ctx.status = 200)) router.get("/version", ctx => (ctx.body = pkg.version)) +router.use(middleware.errorHandling) + router .use( compress({ @@ -54,27 +56,6 @@ router .use(currentApp) .use(auth.auditLog) -// error handling middleware -router.use(async (ctx, next) => { - try { - await next() - } catch (err: any) { - ctx.status = err.status || err.statusCode || 500 - const error = errors.getPublicError(err) - ctx.body = { - message: err.message, - status: ctx.status, - validationErrors: err.validation, - error, - } - ctx.log.error(err) - // unauthorised errors don't provide a useful trace - if (!env.isTest()) { - console.trace(err) - } - } -}) - // authenticated routes for (let route of mainRoutes) { router.use(route.routes()) diff --git a/packages/server/src/tests/jestEnv.ts b/packages/server/src/tests/jestEnv.ts index 9707893bd9..b1ef038c1b 100644 --- a/packages/server/src/tests/jestEnv.ts +++ b/packages/server/src/tests/jestEnv.ts @@ -1,9 +1,9 @@ -import env from "../environment" import { tmpdir } from "os" -env._set("SELF_HOSTED", "1") -env._set("NODE_ENV", "jest") -env._set("MULTI_TENANCY", "1") +process.env.SELF_HOSTED = "1" +process.env.NODE_ENV = "jest" +process.env.MULTI_TENANCY = "1" // @ts-ignore -env._set("BUDIBASE_DIR", tmpdir("budibase-unittests")) -env._set("LOG_LEVEL", "silent") +process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") +process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.MOCK_REDIS = "1" diff --git a/packages/server/src/tests/jestSetup.ts b/packages/server/src/tests/jestSetup.ts index d87ccbfe7c..b052d9941b 100644 --- a/packages/server/src/tests/jestSetup.ts +++ b/packages/server/src/tests/jestSetup.ts @@ -1,3 +1,4 @@ +import "./logging" import env from "../environment" import { env as coreEnv } from "@budibase/backend-core" import { testContainerUtils } from "@budibase/backend-core/tests" diff --git a/packages/server/src/tests/logging.ts b/packages/server/src/tests/logging.ts new file mode 100644 index 0000000000..271f4d62ff --- /dev/null +++ b/packages/server/src/tests/logging.ts @@ -0,0 +1,34 @@ +export enum LogLevel { + TRACE = "trace", + DEBUG = "debug", + INFO = "info", + WARN = "warn", + ERROR = "error", +} + +const LOG_INDEX: { [key in LogLevel]: number } = { + [LogLevel.TRACE]: 1, + [LogLevel.DEBUG]: 2, + [LogLevel.INFO]: 3, + [LogLevel.WARN]: 4, + [LogLevel.ERROR]: 5, +} + +const setIndex = LOG_INDEX[process.env.LOG_LEVEL as LogLevel] + +if (setIndex > LOG_INDEX.trace) { + global.console.trace = jest.fn() +} + +if (setIndex > LOG_INDEX.debug) { + global.console.debug = jest.fn() +} + +if (setIndex > LOG_INDEX.info) { + global.console.info = jest.fn() + global.console.log = jest.fn() +} + +if (setIndex > LOG_INDEX.warn) { + global.console.warn = jest.fn() +} diff --git a/packages/types/src/api/web/errors.ts b/packages/types/src/api/web/errors.ts index 65870d6a29..996c0ba34c 100644 --- a/packages/types/src/api/web/errors.ts +++ b/packages/types/src/api/web/errors.ts @@ -2,4 +2,5 @@ export interface APIError { message: string status: number error?: any + validationErrors?: any } diff --git a/packages/worker/src/api/index.ts b/packages/worker/src/api/index.ts index d8df62f532..b390d36bb8 100644 --- a/packages/worker/src/api/index.ts +++ b/packages/worker/src/api/index.ts @@ -3,8 +3,7 @@ const compress = require("koa-compress") const zlib = require("zlib") import { routes } from "./routes" import { middleware as pro } from "@budibase/pro" -import { errors, auth, middleware } from "@budibase/backend-core" -import { APIError } from "@budibase/types" +import { auth, middleware } from "@budibase/backend-core" const PUBLIC_ENDPOINTS = [ // deprecated single tenant sso callback @@ -109,7 +108,9 @@ const NO_TENANCY_ENDPOINTS = [ const NO_CSRF_ENDPOINTS = [...PUBLIC_ENDPOINTS] const router: Router = new Router() + router + .use(middleware.errorHandling) .use( compress({ threshold: 2048, @@ -136,29 +137,12 @@ router (!ctx.isAuthenticated || (ctx.user && !ctx.user.budibaseAccess)) && !ctx.internal ) { - ctx.throw(403, "Unauthorized - no public worker access") + ctx.throw(403, "Unauthorized") } return next() }) .use(middleware.auditLog) -// error handling middleware - TODO: This could be moved to backend-core -router.use(async (ctx, next) => { - try { - await next() - } catch (err: any) { - ctx.log.error(err) - ctx.status = err.status || err.statusCode || 500 - const error = errors.getPublicError(err) - const body: APIError = { - message: err.message, - status: ctx.status, - error, - } - ctx.body = body - } -}) - router.get("/health", ctx => (ctx.status = 200)) // authenticated routes diff --git a/packages/worker/src/api/routes/system/tests/migrations.spec.ts b/packages/worker/src/api/routes/system/tests/migrations.spec.ts index 304a64761e..950f5c2153 100644 --- a/packages/worker/src/api/routes/system/tests/migrations.spec.ts +++ b/packages/worker/src/api/routes/system/tests/migrations.spec.ts @@ -30,7 +30,7 @@ describe("/api/system/migrations", () => { headers: {}, status: 403, }) - expect(res.text).toBe("Unauthorized - no public worker access") + expect(res.body).toEqual({ message: "Unauthorized", status: 403 }) expect(migrateFn).toBeCalledTimes(0) }) @@ -47,7 +47,7 @@ describe("/api/system/migrations", () => { headers: {}, status: 403, }) - expect(res.text).toBe("Unauthorized - no public worker access") + expect(res.body).toEqual({ message: "Unauthorized", status: 403 }) }) it("returns definitions", async () => { diff --git a/packages/worker/src/middleware/tests/tenancy.spec.ts b/packages/worker/src/middleware/tests/tenancy.spec.ts index a8b7a50e55..8853291634 100644 --- a/packages/worker/src/middleware/tests/tenancy.spec.ts +++ b/packages/worker/src/middleware/tests/tenancy.spec.ts @@ -24,7 +24,7 @@ describe("tenancy middleware", () => { }) it("should get tenant id from header", async () => { - const tenantId = structures.uuid() + const tenantId = structures.tenant.id() const headers = { [constants.Header.TENANT_ID]: tenantId, } @@ -35,7 +35,7 @@ describe("tenancy middleware", () => { }) it("should get tenant id from query param", async () => { - const tenantId = structures.uuid() + const tenantId = structures.tenant.id() const res = await config.request.get( `/api/global/configs/checklist?tenantId=${tenantId}` ) @@ -43,7 +43,7 @@ describe("tenancy middleware", () => { }) it("should get tenant id from subdomain", async () => { - const tenantId = structures.uuid() + const tenantId = structures.tenant.id() const headers = { host: `${tenantId}.localhost:10000`, } @@ -67,7 +67,7 @@ describe("tenancy middleware", () => { it("should throw when no tenant id is found", async () => { const res = await config.request.get(`/api/global/configs/checklist`) expect(res.status).toBe(403) - expect(res.text).toBe("Tenant id not set") + expect(res.body).toEqual({ message: "Tenant id not set", status: 403 }) expect(res.headers[constants.Header.TENANT_ID]).toBe(undefined) }) }) diff --git a/packages/worker/src/tests/jestEnv.ts b/packages/worker/src/tests/jestEnv.ts index 0b27cf52aa..602a505c1b 100644 --- a/packages/worker/src/tests/jestEnv.ts +++ b/packages/worker/src/tests/jestEnv.ts @@ -1,7 +1,7 @@ process.env.SELF_HOSTED = "0" process.env.NODE_ENV = "jest" process.env.JWT_SECRET = "test-jwtsecret" -process.env.LOG_LEVEL = "silent" +process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" process.env.MULTI_TENANCY = "1" process.env.MINIO_URL = "http://localhost" process.env.MINIO_ACCESS_KEY = "test" diff --git a/packages/worker/src/tests/jestSetup.ts b/packages/worker/src/tests/jestSetup.ts index 2deef96176..31d36f00ae 100644 --- a/packages/worker/src/tests/jestSetup.ts +++ b/packages/worker/src/tests/jestSetup.ts @@ -1,5 +1,6 @@ -import { mocks, testContainerUtils } from "@budibase/backend-core/tests" +import "./logging" +import { mocks, testContainerUtils } from "@budibase/backend-core/tests" import env from "../environment" import { env as coreEnv } from "@budibase/backend-core" @@ -11,10 +12,6 @@ mocks.fetch.enable() const tk = require("timekeeper") tk.freeze(mocks.date.MOCK_DATE) -if (!process.env.DEBUG) { - global.console.log = jest.fn() // console.log are ignored in tests -} - if (!process.env.CI) { // set a longer timeout in dev for debugging // 100 seconds diff --git a/packages/worker/src/tests/logging.ts b/packages/worker/src/tests/logging.ts new file mode 100644 index 0000000000..271f4d62ff --- /dev/null +++ b/packages/worker/src/tests/logging.ts @@ -0,0 +1,34 @@ +export enum LogLevel { + TRACE = "trace", + DEBUG = "debug", + INFO = "info", + WARN = "warn", + ERROR = "error", +} + +const LOG_INDEX: { [key in LogLevel]: number } = { + [LogLevel.TRACE]: 1, + [LogLevel.DEBUG]: 2, + [LogLevel.INFO]: 3, + [LogLevel.WARN]: 4, + [LogLevel.ERROR]: 5, +} + +const setIndex = LOG_INDEX[process.env.LOG_LEVEL as LogLevel] + +if (setIndex > LOG_INDEX.trace) { + global.console.trace = jest.fn() +} + +if (setIndex > LOG_INDEX.debug) { + global.console.debug = jest.fn() +} + +if (setIndex > LOG_INDEX.info) { + global.console.info = jest.fn() + global.console.log = jest.fn() +} + +if (setIndex > LOG_INDEX.warn) { + global.console.warn = jest.fn() +} From ae9979929ad92b8be257422e8cd6c6339892e8c4 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 11:57:30 +0000 Subject: [PATCH 02/12] Enable higher concurrency and resiliency in worker tests --- packages/backend-core/package.json | 4 +- .../src/cache/tests/writethrough.spec.js | 61 ------ .../src/cache/tests/writethrough.spec.ts | 73 +++++++ packages/backend-core/src/cache/user.ts | 14 +- .../backend-core/src/context/deprovision.ts | 108 ---------- .../src/context/tests/index.spec.ts | 11 +- packages/backend-core/src/db/db.ts | 3 +- .../src/db/tests/utils.seq.spec.ts | 190 ----------------- .../backend-core/src/db/tests/utils.spec.ts | 192 +++++++++++++++++ packages/backend-core/src/db/views.ts | 64 +++--- packages/backend-core/src/events/analytics.ts | 4 +- .../posthog/tests/PosthogProcessor.spec.ts | 6 +- .../backend-core/src/featureFlags/index.ts | 4 +- packages/backend-core/src/index.ts | 20 +- .../src/middleware/authenticated.ts | 2 +- .../src/middleware/passport/utils.ts | 4 +- .../backend-core/src/middleware/tenancy.ts | 3 +- .../backend-core/src/migrations/migrations.ts | 8 +- .../src/objectStore/buckets/global.ts | 4 +- .../src/objectStore/buckets/plugins.ts | 4 +- packages/backend-core/src/platform/index.ts | 3 + .../backend-core/src/platform/platformDb.ts | 6 + packages/backend-core/src/platform/tenants.ts | 101 +++++++++ .../src/platform/tests/tenants.spec.ts | 25 +++ packages/backend-core/src/platform/users.ts | 90 ++++++++ packages/backend-core/src/redis/index.ts | 2 +- packages/backend-core/src/redis/redlock.ts | 39 ++-- packages/backend-core/src/tenancy/db.ts | 6 + packages/backend-core/src/tenancy/index.ts | 2 +- packages/backend-core/src/tenancy/tenancy.ts | 102 +-------- .../src/utils/tests/utils.spec.ts | 46 ++-- .../tests/utilities/DBTestConfiguration.ts | 32 +++ packages/backend-core/tests/utilities/db.ts | 9 - .../backend-core/tests/utilities/index.ts | 3 +- .../backend-core/tests/utilities/testEnv.ts | 16 +- packages/backend-core/yarn.lock | 8 +- packages/server/package.json | 3 +- .../src/api/routes/tests/backup.spec.ts | 1 - .../src/tests/utilities/TestConfiguration.ts | 96 +++++---- packages/server/yarn.lock | 109 ++++++---- packages/types/src/sdk/locks.ts | 1 + packages/worker/package.json | 5 +- .../src/api/controllers/global/users.ts | 49 +++-- .../src/api/controllers/system/tenants.ts | 12 +- .../api/routes/global/tests/configs.spec.ts | 198 ++++++++---------- .../src/api/routes/global/tests/roles.spec.ts | 5 +- .../src/api/routes/global/tests/users.spec.ts | 84 ++++---- .../worker/src/api/routes/system/tenants.ts | 2 +- .../api/routes/system/tests/restore.spec.ts | 4 +- .../api/routes/system/tests/status.spec.ts | 10 +- .../functions/globalInfoSyncUsers.ts | 7 +- packages/worker/src/sdk/tenants/index.ts | 1 + packages/worker/src/sdk/tenants/tenants.ts | 76 +++++++ packages/worker/src/sdk/users/users.ts | 2 +- .../worker/src/tests/TestConfiguration.ts | 121 ++++------- packages/worker/src/tests/api/base.ts | 3 +- packages/worker/src/tests/api/restore.ts | 1 + packages/worker/yarn.lock | 110 +++++----- 58 files changed, 1165 insertions(+), 1004 deletions(-) delete mode 100644 packages/backend-core/src/cache/tests/writethrough.spec.js create mode 100644 packages/backend-core/src/cache/tests/writethrough.spec.ts delete mode 100644 packages/backend-core/src/context/deprovision.ts delete mode 100644 packages/backend-core/src/db/tests/utils.seq.spec.ts create mode 100644 packages/backend-core/src/db/tests/utils.spec.ts create mode 100644 packages/backend-core/src/platform/index.ts create mode 100644 packages/backend-core/src/platform/platformDb.ts create mode 100644 packages/backend-core/src/platform/tenants.ts create mode 100644 packages/backend-core/src/platform/tests/tenants.spec.ts create mode 100644 packages/backend-core/src/platform/users.ts create mode 100644 packages/backend-core/src/tenancy/db.ts create mode 100644 packages/backend-core/tests/utilities/DBTestConfiguration.ts delete mode 100644 packages/backend-core/tests/utilities/db.ts create mode 100644 packages/worker/src/sdk/tenants/index.ts create mode 100644 packages/worker/src/sdk/tenants/tenants.ts diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index e20798f3ac..ac87025303 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -18,7 +18,7 @@ "build:pro": "../../scripts/pro/build.sh", "postbuild": "yarn run build:pro", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", - "test": "jest --coverage --maxWorkers=2", + "test": "jest --coverage", "test:watch": "jest --watchAll" }, "dependencies": { @@ -62,7 +62,7 @@ "@trendyol/jest-testcontainers": "^2.1.1", "@types/chance": "1.1.3", "@types/ioredis": "4.28.0", - "@types/jest": "27.5.1", + "@types/jest": "28.1.1", "@types/koa": "2.13.4", "@types/koa-pino-logger": "3.0.0", "@types/lodash": "4.14.180", diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.js b/packages/backend-core/src/cache/tests/writethrough.spec.js deleted file mode 100644 index fefca30c18..0000000000 --- a/packages/backend-core/src/cache/tests/writethrough.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -require("../../../tests") -const { Writethrough } = require("../writethrough") -const { getDB } = require("../../db") -const tk = require("timekeeper") -const { structures } = require("../../../tests") - -const START_DATE = Date.now() -tk.freeze(START_DATE) - - -const DELAY = 5000 - -const db = getDB(structures.db.id()) -const db2 = getDB(structures.db.id()) -const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY) - -describe("writethrough", () => { - describe("put", () => { - let first - it("should be able to store, will go to DB", async () => { - const response = await writethrough.put({ _id: "test", value: 1 }) - const output = await db.get(response.id) - first = output - expect(output.value).toBe(1) - }) - - it("second put shouldn't update DB", async () => { - const response = await writethrough.put({ ...first, value: 2 }) - const output = await db.get(response.id) - expect(first._rev).toBe(output._rev) - expect(output.value).toBe(1) - }) - - it("should put it again after delay period", async () => { - tk.freeze(START_DATE + DELAY + 1) - const response = await writethrough.put({ ...first, value: 3 }) - const output = await db.get(response.id) - expect(response.rev).not.toBe(first._rev) - expect(output.value).toBe(3) - }) - }) - - describe("get", () => { - it("should be able to retrieve", async () => { - const response = await writethrough.get("test") - expect(response.value).toBe(3) - }) - }) - - describe("same doc, different databases (tenancy)", () => { - it("should be able to two different databases", async () => { - const resp1 = await writethrough.put({ _id: "db1", value: "first" }) - const resp2 = await writethrough2.put({ _id: "db1", value: "second" }) - expect(resp1.rev).toBeDefined() - expect(resp2.rev).toBeDefined() - expect((await db.get("db1")).value).toBe("first") - expect((await db2.get("db1")).value).toBe("second") - }) - }) -}) - diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.ts b/packages/backend-core/src/cache/tests/writethrough.spec.ts new file mode 100644 index 0000000000..d346788121 --- /dev/null +++ b/packages/backend-core/src/cache/tests/writethrough.spec.ts @@ -0,0 +1,73 @@ +import { structures, DBTestConfiguration } from "../../../tests" +import { Writethrough } from "../writethrough" +import { getDB } from "../../db" +import tk from "timekeeper" + +const START_DATE = Date.now() +tk.freeze(START_DATE) + +const DELAY = 5000 + +describe("writethrough", () => { + const config = new DBTestConfiguration() + + const db = getDB(structures.db.id()) + const db2 = getDB(structures.db.id()) + + const writethrough = new Writethrough(db, DELAY) + const writethrough2 = new Writethrough(db2, DELAY) + + describe("put", () => { + let first: any + + it("should be able to store, will go to DB", async () => { + await config.doInTenant(async () => { + const response = await writethrough.put({ _id: "test", value: 1 }) + const output = await db.get(response.id) + first = output + expect(output.value).toBe(1) + }) + }) + + it("second put shouldn't update DB", async () => { + await config.doInTenant(async () => { + const response = await writethrough.put({ ...first, value: 2 }) + const output = await db.get(response.id) + expect(first._rev).toBe(output._rev) + expect(output.value).toBe(1) + }) + }) + + it("should put it again after delay period", async () => { + await config.doInTenant(async () => { + tk.freeze(START_DATE + DELAY + 1) + const response = await writethrough.put({ ...first, value: 3 }) + const output = await db.get(response.id) + expect(response.rev).not.toBe(first._rev) + expect(output.value).toBe(3) + }) + }) + }) + + describe("get", () => { + it("should be able to retrieve", async () => { + await config.doInTenant(async () => { + const response = await writethrough.get("test") + expect(response.value).toBe(3) + }) + }) + }) + + describe("same doc, different databases (tenancy)", () => { + it("should be able to two different databases", async () => { + await config.doInTenant(async () => { + const resp1 = await writethrough.put({ _id: "db1", value: "first" }) + const resp2 = await writethrough2.put({ _id: "db1", value: "second" }) + expect(resp1.rev).toBeDefined() + expect(resp2.rev).toBeDefined() + expect((await db.get("db1")).value).toBe("first") + expect((await db2.get("db1")).value).toBe("second") + }) + }) + }) +}) diff --git a/packages/backend-core/src/cache/user.ts b/packages/backend-core/src/cache/user.ts index a128465cd6..b514c3af9b 100644 --- a/packages/backend-core/src/cache/user.ts +++ b/packages/backend-core/src/cache/user.ts @@ -1,8 +1,9 @@ import * as redis from "../redis/init" -import { getTenantId, lookupTenantId, doWithGlobalDB } from "../tenancy" +import * as tenancy from "../tenancy" +import * as context from "../context" +import * as platform from "../platform" import env from "../environment" -import * as accounts from "../cloud/accounts" -import { Database } from "@budibase/types" +import * as accounts from "../accounts" const EXPIRY_SECONDS = 3600 @@ -10,7 +11,8 @@ const EXPIRY_SECONDS = 3600 * The default populate user function */ async function populateFromDB(userId: string, tenantId: string) { - const user = await doWithGlobalDB(tenantId, (db: Database) => db.get(userId)) + const db = tenancy.getTenantDB(tenantId) + const user = await db.get(userId) user.budibaseAccess = true if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const account = await accounts.getAccount(user.email) @@ -42,9 +44,9 @@ export async function getUser( } if (!tenantId) { try { - tenantId = getTenantId() + tenantId = context.getTenantId() } catch (err) { - tenantId = await lookupTenantId(userId) + tenantId = await platform.users.lookupTenantId(userId) } } const client = await redis.getUserClient() diff --git a/packages/backend-core/src/context/deprovision.ts b/packages/backend-core/src/context/deprovision.ts deleted file mode 100644 index 81f03096dc..0000000000 --- a/packages/backend-core/src/context/deprovision.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - getGlobalUserParams, - getAllApps, - doWithDB, - StaticDatabases, -} from "../db" -import { doWithGlobalDB } from "../tenancy" -import { App, Tenants, User, Database } from "@budibase/types" - -const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants -const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name - -async function removeTenantFromInfoDB(tenantId: string) { - try { - await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => { - const tenants = (await infoDb.get(TENANT_DOC)) as Tenants - tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) - - await infoDb.put(tenants) - }) - } catch (err) { - console.error(`Error removing tenant ${tenantId} from info db`, err) - throw err - } -} - -export async function removeUserFromInfoDB(dbUser: User) { - await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => { - const keys = [dbUser._id!, dbUser.email] - const userDocs = await infoDb.allDocs({ - keys, - include_docs: true, - }) - const toDelete = userDocs.rows.map((row: any) => { - return { - ...row.doc, - _deleted: true, - } - }) - await infoDb.bulkDocs(toDelete) - }) -} - -async function removeUsersFromInfoDB(tenantId: string) { - return doWithGlobalDB(tenantId, async (db: any) => { - try { - const allUsers = await db.allDocs( - getGlobalUserParams(null, { - include_docs: true, - }) - ) - await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { - const allEmails = allUsers.rows.map((row: any) => row.doc.email) - // get the id docs - let keys = allUsers.rows.map((row: any) => row.id) - // and the email docs - keys = keys.concat(allEmails) - // retrieve the docs and delete them - const userDocs = await infoDb.allDocs({ - keys, - include_docs: true, - }) - const toDelete = userDocs.rows.map((row: any) => { - return { - ...row.doc, - _deleted: true, - } - }) - await infoDb.bulkDocs(toDelete) - }) - } catch (err) { - console.error(`Error removing tenant ${tenantId} users from info db`, err) - throw err - } - }) -} - -async function removeGlobalDB(tenantId: string) { - return doWithGlobalDB(tenantId, async (db: Database) => { - try { - await db.destroy() - } catch (err) { - console.error(`Error removing tenant ${tenantId} users from info db`, err) - throw err - } - }) -} - -async function removeTenantApps(tenantId: string) { - try { - const apps = (await getAllApps({ all: true })) as App[] - const destroyPromises = apps.map(app => - doWithDB(app.appId, (db: Database) => db.destroy()) - ) - await Promise.allSettled(destroyPromises) - } catch (err) { - console.error(`Error removing tenant ${tenantId} apps`, err) - throw err - } -} - -// can't live in tenancy package due to circular dependency on db/utils -export async function deleteTenant(tenantId: string) { - await removeTenantFromInfoDB(tenantId) - await removeUsersFromInfoDB(tenantId) - await removeGlobalDB(tenantId) - await removeTenantApps(tenantId) -} diff --git a/packages/backend-core/src/context/tests/index.spec.ts b/packages/backend-core/src/context/tests/index.spec.ts index c9b5870ffa..5c8ce6fc19 100644 --- a/packages/backend-core/src/context/tests/index.spec.ts +++ b/packages/backend-core/src/context/tests/index.spec.ts @@ -1,11 +1,14 @@ -require("../../../tests") +import { testEnv } from "../../../tests" const context = require("../") const { DEFAULT_TENANT_ID } = require("../../constants") -import env from "../../environment" describe("context", () => { describe("doInTenant", () => { describe("single-tenancy", () => { + beforeAll(() => { + testEnv.singleTenant() + }) + it("defaults to the default tenant", () => { const tenantId = context.getTenantId() expect(tenantId).toBe(DEFAULT_TENANT_ID) @@ -20,8 +23,8 @@ describe("context", () => { }) describe("multi-tenancy", () => { - beforeEach(() => { - env._set("MULTI_TENANCY", 1) + beforeAll(() => { + testEnv.multiTenant() }) it("fails when no tenant id is set", () => { diff --git a/packages/backend-core/src/db/db.ts b/packages/backend-core/src/db/db.ts index bd6b5e13c1..f13eb9a965 100644 --- a/packages/backend-core/src/db/db.ts +++ b/packages/backend-core/src/db/db.ts @@ -1,7 +1,6 @@ import env from "../environment" -import { directCouchQuery, getPouchDB } from "./couch" +import { directCouchQuery, DatabaseImpl } from "./couch" import { CouchFindOptions, Database } from "@budibase/types" -import { DatabaseImpl } from "../db" const dbList = new Set() diff --git a/packages/backend-core/src/db/tests/utils.seq.spec.ts b/packages/backend-core/src/db/tests/utils.seq.spec.ts deleted file mode 100644 index 83253402f7..0000000000 --- a/packages/backend-core/src/db/tests/utils.seq.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -require("../../../tests") -const { - getDevelopmentAppID, - getProdAppID, - isDevAppID, - isProdAppID, -} = require("../conversions") -const { generateAppID, getPlatformUrl, getScopedConfig } = require("../utils") -const tenancy = require("../../tenancy") -const { Config, DEFAULT_TENANT_ID } = require("../../constants") -import { generator } from "../../../tests" -import env from "../../environment" - -describe("utils", () => { - describe("app ID manipulation", () => { - function getID() { - const appId = generateAppID() - const split = appId.split("_") - const uuid = split[split.length - 1] - const devAppId = `app_dev_${uuid}` - return { appId, devAppId, split, uuid } - } - - it("should be able to generate a new app ID", () => { - expect(generateAppID().startsWith("app_")).toEqual(true) - }) - - it("should be able to convert a production app ID to development", () => { - const { appId, uuid } = getID() - expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) - }) - - it("should be able to convert a development app ID to development", () => { - const { devAppId, uuid } = getID() - expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) - }) - - it("should be able to convert a development ID to a production", () => { - const { devAppId, uuid } = getID() - expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) - }) - - it("should be able to convert a production ID to production", () => { - const { appId, uuid } = getID() - expect(getProdAppID(appId)).toEqual(`app_${uuid}`) - }) - - it("should be able to confirm dev app ID is development", () => { - const { devAppId } = getID() - expect(isDevAppID(devAppId)).toEqual(true) - }) - - it("should be able to confirm prod app ID is not development", () => { - const { appId } = getID() - expect(isDevAppID(appId)).toEqual(false) - }) - - it("should be able to confirm prod app ID is prod", () => { - const { appId } = getID() - expect(isProdAppID(appId)).toEqual(true) - }) - - it("should be able to confirm dev app ID is not prod", () => { - const { devAppId } = getID() - expect(isProdAppID(devAppId)).toEqual(false) - }) - }) -}) - -const DEFAULT_URL = "http://localhost:10000" -const ENV_URL = "http://env.com" - -const setDbPlatformUrl = async (dbUrl: string) => { - const db = tenancy.getGlobalDB() - await db.put({ - _id: "config_settings", - type: Config.SETTINGS, - config: { - platformUrl: dbUrl, - }, - }) -} - -const clearSettingsConfig = async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const db = tenancy.getGlobalDB() - try { - const config = await db.get("config_settings") - await db.remove("config_settings", config._rev) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - }) -} - -describe("getPlatformUrl", () => { - describe("self host", () => { - beforeEach(async () => { - env._set("SELF_HOST", 1) - await clearSettingsConfig() - }) - - it("gets the default url", async () => { - await tenancy.doInTenant(null, async () => { - const url = await getPlatformUrl() - expect(url).toBe(DEFAULT_URL) - }) - }) - - it("gets the platform url from the environment", async () => { - await tenancy.doInTenant(null, async () => { - env._set("PLATFORM_URL", ENV_URL) - const url = await getPlatformUrl() - expect(url).toBe(ENV_URL) - }) - }) - - it("gets the platform url from the database", async () => { - await tenancy.doInTenant(null, async () => { - const dbUrl = generator.url() - await setDbPlatformUrl(dbUrl) - const url = await getPlatformUrl() - expect(url).toBe(dbUrl) - }) - }) - }) - - describe("cloud", () => { - const TENANT_AWARE_URL = "http://default.env.com" - - beforeEach(async () => { - env._set("SELF_HOSTED", 0) - env._set("MULTI_TENANCY", 1) - env._set("PLATFORM_URL", ENV_URL) - await clearSettingsConfig() - }) - - it("gets the platform url from the environment without tenancy", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const url = await getPlatformUrl({ tenantAware: false }) - expect(url).toBe(ENV_URL) - }) - }) - - it("gets the platform url from the environment with tenancy", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const url = await getPlatformUrl() - expect(url).toBe(TENANT_AWARE_URL) - }) - }) - - it("never gets the platform url from the database", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - await setDbPlatformUrl(generator.url()) - const url = await getPlatformUrl() - expect(url).toBe(TENANT_AWARE_URL) - }) - }) - }) -}) - -describe("getScopedConfig", () => { - describe("settings config", () => { - beforeEach(async () => { - env._set("SELF_HOSTED", 1) - env._set("PLATFORM_URL", "") - await clearSettingsConfig() - }) - - it("returns the platform url with an existing config", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const dbUrl = generator.url() - await setDbPlatformUrl(dbUrl) - const db = tenancy.getGlobalDB() - const config = await getScopedConfig(db, { type: Config.SETTINGS }) - expect(config.platformUrl).toBe(dbUrl) - }) - }) - - it("returns the platform url without an existing config", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const db = tenancy.getGlobalDB() - const config = await getScopedConfig(db, { type: Config.SETTINGS }) - expect(config.platformUrl).toBe(DEFAULT_URL) - }) - }) - }) -}) diff --git a/packages/backend-core/src/db/tests/utils.spec.ts b/packages/backend-core/src/db/tests/utils.spec.ts new file mode 100644 index 0000000000..7bdca5ae8b --- /dev/null +++ b/packages/backend-core/src/db/tests/utils.spec.ts @@ -0,0 +1,192 @@ +import { generator, DBTestConfiguration, testEnv } from "../../../tests" +import { + getDevelopmentAppID, + getProdAppID, + isDevAppID, + isProdAppID, +} from "../conversions" +import { generateAppID, getPlatformUrl, getScopedConfig } from "../utils" +import * as context from "../../context" +import { Config } from "../../constants" +import env from "../../environment" + +describe("utils", () => { + const config = new DBTestConfiguration() + + describe("app ID manipulation", () => { + function getID() { + const appId = generateAppID() + const split = appId.split("_") + const uuid = split[split.length - 1] + const devAppId = `app_dev_${uuid}` + return { appId, devAppId, split, uuid } + } + + it("should be able to generate a new app ID", () => { + expect(generateAppID().startsWith("app_")).toEqual(true) + }) + + it("should be able to convert a production app ID to development", () => { + const { appId, uuid } = getID() + expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) + }) + + it("should be able to convert a development app ID to development", () => { + const { devAppId, uuid } = getID() + expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) + }) + + it("should be able to convert a development ID to a production", () => { + const { devAppId, uuid } = getID() + expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) + }) + + it("should be able to convert a production ID to production", () => { + const { appId, uuid } = getID() + expect(getProdAppID(appId)).toEqual(`app_${uuid}`) + }) + + it("should be able to confirm dev app ID is development", () => { + const { devAppId } = getID() + expect(isDevAppID(devAppId)).toEqual(true) + }) + + it("should be able to confirm prod app ID is not development", () => { + const { appId } = getID() + expect(isDevAppID(appId)).toEqual(false) + }) + + it("should be able to confirm prod app ID is prod", () => { + const { appId } = getID() + expect(isProdAppID(appId)).toEqual(true) + }) + + it("should be able to confirm dev app ID is not prod", () => { + const { devAppId } = getID() + expect(isProdAppID(devAppId)).toEqual(false) + }) + }) + + const DEFAULT_URL = "http://localhost:10000" + const ENV_URL = "http://env.com" + + const setDbPlatformUrl = async (dbUrl: string) => { + const db = context.getGlobalDB() + await db.put({ + _id: "config_settings", + type: Config.SETTINGS, + config: { + platformUrl: dbUrl, + }, + }) + } + + const clearSettingsConfig = async () => { + await config.doInTenant(async () => { + const db = context.getGlobalDB() + try { + const config = await db.get("config_settings") + await db.remove("config_settings", config._rev) + } catch (e: any) { + if (e.status !== 404) { + throw e + } + } + }) + } + + describe("getPlatformUrl", () => { + describe("self host", () => { + beforeEach(async () => { + testEnv.selfHosted() + await clearSettingsConfig() + }) + + it("gets the default url", async () => { + await config.doInTenant(async () => { + const url = await getPlatformUrl() + expect(url).toBe(DEFAULT_URL) + }) + }) + + it("gets the platform url from the environment", async () => { + await config.doInTenant(async () => { + env._set("PLATFORM_URL", ENV_URL) + const url = await getPlatformUrl() + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the database", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const url = await getPlatformUrl() + expect(url).toBe(dbUrl) + }) + }) + }) + + describe("cloud", () => { + const TENANT_AWARE_URL = `http://${config.tenantId}.env.com` + + beforeEach(async () => { + testEnv.cloudHosted() + testEnv.multiTenant() + + env._set("PLATFORM_URL", ENV_URL) + await clearSettingsConfig() + }) + + it("gets the platform url from the environment without tenancy", async () => { + await config.doInTenant(async () => { + const url = await getPlatformUrl({ tenantAware: false }) + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the environment with tenancy", async () => { + await config.doInTenant(async () => { + const url = await getPlatformUrl() + expect(url).toBe(TENANT_AWARE_URL) + }) + }) + + it("never gets the platform url from the database", async () => { + await config.doInTenant(async () => { + await setDbPlatformUrl(generator.url()) + const url = await getPlatformUrl() + expect(url).toBe(TENANT_AWARE_URL) + }) + }) + }) + }) + + describe("getScopedConfig", () => { + describe("settings config", () => { + beforeEach(async () => { + env._set("SELF_HOSTED", 1) + env._set("PLATFORM_URL", "") + await clearSettingsConfig() + }) + + it("returns the platform url with an existing config", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const db = context.getGlobalDB() + const config = await getScopedConfig(db, { type: Config.SETTINGS }) + expect(config.platformUrl).toBe(dbUrl) + }) + }) + + it("returns the platform url without an existing config", async () => { + await config.doInTenant(async () => { + const db = context.getGlobalDB() + const config = await getScopedConfig(db, { type: Config.SETTINGS }) + expect(config.platformUrl).toBe(DEFAULT_URL) + }) + }) + }) + }) +}) diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index 4a87be0a68..8a2c2e7efd 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -1,13 +1,14 @@ import { - DocumentType, - ViewName, DeprecatedViews, + DocumentType, SEPARATOR, StaticDatabases, + ViewName, } from "../constants" import { getGlobalDB } from "../context" import { doWithDB } from "./" import { Database, DatabaseQueryOpts } from "@budibase/types" +import env from "../environment" const DESIGN_DB = "_design/database" @@ -69,17 +70,6 @@ export const createNewUserEmailView = async () => { await createView(db, viewJs, ViewName.USER_BY_EMAIL) } -export const createAccountEmailView = async () => { - const viewJs = `function(doc) { - if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { - emit(doc.email.toLowerCase(), doc._id) - } - }` - await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { - await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) - }) -} - export const createUserAppView = async () => { const db = getGlobalDB() const viewJs = `function(doc) { @@ -113,17 +103,6 @@ export const createUserBuildersView = async () => { await createView(db, viewJs, ViewName.USER_BY_BUILDERS) } -export const createPlatformUserView = async () => { - const viewJs = `function(doc) { - if (doc.tenantId) { - emit(doc._id.toLowerCase(), doc._id) - } - }` - await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { - await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) - }) -} - export interface QueryViewOptions { arrayResponse?: boolean } @@ -162,13 +141,48 @@ export const queryView = async ( } } +// PLATFORM + +async function createPlatformView(viewJs: string, viewName: ViewName) { + try { + await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { + await createView(db, viewJs, viewName) + }) + } catch (e: any) { + if (e.status === 409 && env.isTest()) { + // multiple tests can try to initialise platforms views + // at once - safe to exit on conflict + return + } + throw e + } +} + +export const createPlatformAccountEmailView = async () => { + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }` + await createPlatformView(viewJs, ViewName.ACCOUNT_BY_EMAIL) +} + +export const createPlatformUserView = async () => { + const viewJs = `function(doc) { + if (doc.tenantId) { + emit(doc._id.toLowerCase(), doc._id) + } + }` + await createPlatformView(viewJs, ViewName.PLATFORM_USERS_LOWERCASE) +} + export const queryPlatformView = async ( viewName: ViewName, params: DatabaseQueryOpts, opts?: QueryViewOptions ): Promise => { const CreateFuncByName: any = { - [ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView, + [ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, } diff --git a/packages/backend-core/src/events/analytics.ts b/packages/backend-core/src/events/analytics.ts index f621a9c98b..7fbc6d9c2b 100644 --- a/packages/backend-core/src/events/analytics.ts +++ b/packages/backend-core/src/events/analytics.ts @@ -1,5 +1,5 @@ import env from "../environment" -import * as tenancy from "../tenancy" +import * as context from "../context" import * as dbUtils from "../db/utils" import { Config } from "../constants" import { withCache, TTL, CacheKey } from "../cache" @@ -42,7 +42,7 @@ export const enabled = async () => { } const getSettingsDoc = async () => { - const db = tenancy.getGlobalDB() + const db = context.getGlobalDB() let settings try { settings = await db.get(dbUtils.generateConfigID({ type: Config.SETTINGS })) diff --git a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts index 349a0427ac..2c1340d36e 100644 --- a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +++ b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts @@ -1,4 +1,4 @@ -import "../../../../../tests" +import { testEnv } from "../../../../../tests" import PosthogProcessor from "../PosthogProcessor" import { Event, IdentityType, Hosting } from "@budibase/types" const tk = require("timekeeper") @@ -16,6 +16,10 @@ const newIdentity = () => { } describe("PosthogProcessor", () => { + beforeAll(() => { + testEnv.singleTenant() + }) + beforeEach(async () => { jest.clearAllMocks() await cache.bustCache( diff --git a/packages/backend-core/src/featureFlags/index.ts b/packages/backend-core/src/featureFlags/index.ts index 34ee3599a5..877cd60e1a 100644 --- a/packages/backend-core/src/featureFlags/index.ts +++ b/packages/backend-core/src/featureFlags/index.ts @@ -1,5 +1,5 @@ import env from "../environment" -import * as tenancy from "../tenancy" +import * as context from "../context" /** * Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant. @@ -28,7 +28,7 @@ export function buildFeatureFlags() { } export function isEnabled(featureFlag: string) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() const flags = getTenantFeatureFlags(tenantId) return flags.includes(featureFlag) } diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index b38a53e9e4..d507d8175f 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -3,12 +3,11 @@ export * as migrations from "./migrations" export * as users from "./users" export * as roles from "./security/roles" export * as permissions from "./security/permissions" -export * as accounts from "./cloud/accounts" +export * as accounts from "./accounts" export * as installation from "./installation" -export * as tenancy from "./tenancy" export * as featureFlags from "./featureFlags" export * as sessions from "./security/sessions" -export * as deprovisioning from "./context/deprovision" +export * as platform from "./platform" export * as auth from "./auth" export * as constants from "./constants" export * as logging from "./logging" @@ -21,20 +20,27 @@ export * as context from "./context" export * as cache from "./cache" export * as objectStore from "./objectStore" export * as redis from "./redis" +export * as locks from "./redis/redlock" export * as utils from "./utils" export * as errors from "./errors" export { default as env } from "./environment" +// Add context to tenancy for backwards compatibility +// only do this for external usages to prevent internal +// circular dependencies +import * as context from "./context" +import * as _tenancy from "./tenancy" +export const tenancy = { + ..._tenancy, + ...context, +} + // expose error classes directly export * from "./errors" // expose constants directly export * from "./constants" -// expose inner locks from redis directly -import * as redis from "./redis" -export const locks = redis.redlock - // expose package init function import * as db from "./db" export const init = (opts: any = {}) => { diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 3b5e9ae162..4bb2aaba76 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -4,7 +4,7 @@ import { getUser } from "../cache/user" import { getSession, updateSessionTTL } from "../security/sessions" import { buildMatcherRegex, matches } from "./matchers" import { SEPARATOR, queryGlobalView, ViewName } from "../db" -import { getGlobalDB, doInTenant } from "../tenancy" +import { getGlobalDB, doInTenant } from "../context" import { decrypt } from "../security/encryption" import * as identity from "../context/identity" import env from "../environment" diff --git a/packages/backend-core/src/middleware/passport/utils.ts b/packages/backend-core/src/middleware/passport/utils.ts index 3d79aada28..6eb3bc29d1 100644 --- a/packages/backend-core/src/middleware/passport/utils.ts +++ b/packages/backend-core/src/middleware/passport/utils.ts @@ -1,6 +1,6 @@ -import { isMultiTenant, getTenantId } from "../../tenancy" +import { isMultiTenant, getTenantId } from "../../context" import { getScopedConfig } from "../../db" -import { ConfigType, Database, Config } from "@budibase/types" +import { ConfigType, Database } from "@budibase/types" /** * Utility to handle authentication errors. diff --git a/packages/backend-core/src/middleware/tenancy.ts b/packages/backend-core/src/middleware/tenancy.ts index a09c463045..22b7cc213d 100644 --- a/packages/backend-core/src/middleware/tenancy.ts +++ b/packages/backend-core/src/middleware/tenancy.ts @@ -1,4 +1,5 @@ -import { doInTenant, getTenantIDFromCtx } from "../tenancy" +import { doInTenant } from "../context" +import { getTenantIDFromCtx } from "../tenancy" import { buildMatcherRegex, matches } from "./matchers" import { Header } from "../constants" import { diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts index 55b8ab1938..79c7eb55ea 100644 --- a/packages/backend-core/src/migrations/migrations.ts +++ b/packages/backend-core/src/migrations/migrations.ts @@ -7,7 +7,7 @@ import { doWithDB, } from "../db" import environment from "../environment" -import { doInTenant, getTenantIds, getTenantId } from "../tenancy" +import * as platform from "../platform" import * as context from "../context" import { DEFINITIONS } from "." import { @@ -47,7 +47,7 @@ export const runMigration = async ( const migrationType = migration.type let tenantId: string | undefined if (migrationType !== MigrationType.INSTALLATION) { - tenantId = getTenantId() + tenantId = context.getTenantId() } const migrationName = migration.name const silent = migration.silent @@ -160,7 +160,7 @@ export const runMigrations = async ( tenantIds = [options.noOp.tenantId] } else if (!options.tenantIds || !options.tenantIds.length) { // run for all tenants - tenantIds = await getTenantIds() + tenantIds = await platform.tenants.getTenantIds() } else { tenantIds = options.tenantIds } @@ -185,7 +185,7 @@ export const runMigrations = async ( // for all migrations for (const migration of migrations) { // run the migration - await doInTenant(tenantId, () => runMigration(migration, options)) + await context.doInTenant(tenantId, () => runMigration(migration, options)) } } console.log("Migrations complete") diff --git a/packages/backend-core/src/objectStore/buckets/global.ts b/packages/backend-core/src/objectStore/buckets/global.ts index 8bf883b11e..69e201bb98 100644 --- a/packages/backend-core/src/objectStore/buckets/global.ts +++ b/packages/backend-core/src/objectStore/buckets/global.ts @@ -1,5 +1,5 @@ import env from "../../environment" -import * as tenancy from "../../tenancy" +import * as context from "../../context" import * as objectStore from "../objectStore" import * as cloudfront from "../cloudfront" @@ -22,7 +22,7 @@ export const getGlobalFileUrl = (type: string, name: string, etag?: string) => { export const getGlobalFileS3Key = (type: string, name: string) => { let file = `${type}/${name}` if (env.MULTI_TENANCY) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() file = `${tenantId}/${file}` } return file diff --git a/packages/backend-core/src/objectStore/buckets/plugins.ts b/packages/backend-core/src/objectStore/buckets/plugins.ts index cd3bf77e87..f7721afb23 100644 --- a/packages/backend-core/src/objectStore/buckets/plugins.ts +++ b/packages/backend-core/src/objectStore/buckets/plugins.ts @@ -1,6 +1,6 @@ import env from "../../environment" import * as objectStore from "../objectStore" -import * as tenancy from "../../tenancy" +import * as context from "../../context" import * as cloudfront from "../cloudfront" import { Plugin } from "@budibase/types" @@ -61,7 +61,7 @@ const getPluginS3Key = (plugin: Plugin, fileName: string) => { export const getPluginS3Dir = (pluginName: string) => { let s3Key = `${pluginName}` if (env.MULTI_TENANCY) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() s3Key = `${tenantId}/${s3Key}` } if (env.CLOUDFRONT_CDN) { diff --git a/packages/backend-core/src/platform/index.ts b/packages/backend-core/src/platform/index.ts new file mode 100644 index 0000000000..877d85ade0 --- /dev/null +++ b/packages/backend-core/src/platform/index.ts @@ -0,0 +1,3 @@ +export * as users from "./users" +export * as tenants from "./tenants" +export * from "./platformDb" diff --git a/packages/backend-core/src/platform/platformDb.ts b/packages/backend-core/src/platform/platformDb.ts new file mode 100644 index 0000000000..90b683dd33 --- /dev/null +++ b/packages/backend-core/src/platform/platformDb.ts @@ -0,0 +1,6 @@ +import { StaticDatabases } from "../constants" +import { getDB } from "../db/db" + +export function getPlatformDB() { + return getDB(StaticDatabases.PLATFORM_INFO.name) +} diff --git a/packages/backend-core/src/platform/tenants.ts b/packages/backend-core/src/platform/tenants.ts new file mode 100644 index 0000000000..b9f946a735 --- /dev/null +++ b/packages/backend-core/src/platform/tenants.ts @@ -0,0 +1,101 @@ +import { StaticDatabases } from "../constants" +import { getPlatformDB } from "./platformDb" +import { LockName, LockOptions, LockType, Tenants } from "@budibase/types" +import * as locks from "../redis/redlock" + +const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants + +export const tenacyLockOptions: LockOptions = { + type: LockType.DEFAULT, + name: LockName.UPDATE_TENANTS_DOC, + ttl: 10 * 1000, // auto expire after 10 seconds + systemLock: true, +} + +// READ + +export async function getTenantIds(): Promise { + const tenants = await getTenants() + return tenants.tenantIds +} + +async function getTenants(): Promise { + const db = getPlatformDB() + let tenants: Tenants + + try { + tenants = await db.get(TENANT_DOC) + } catch (e: any) { + // doesn't exist yet - create + if (e.status === 404) { + tenants = await createTenantsDoc() + } else { + throw e + } + } + + return tenants +} + +export async function exists(tenantId: string) { + const tenants = await getTenants() + return tenants.tenantIds.indexOf(tenantId) !== -1 +} + +// CREATE / UPDATE + +function newTenantsDoc(): Tenants { + return { + _id: TENANT_DOC, + tenantIds: [], + } +} + +async function createTenantsDoc(): Promise { + const db = getPlatformDB() + let tenants = newTenantsDoc() + + try { + const response = await db.put(tenants) + tenants._rev = response.rev + } catch (e: any) { + // don't throw 409 is doc has already been created + if (e.status === 409) { + return db.get(TENANT_DOC) + } + throw e + } + + return tenants +} + +export async function addTenant(tenantId: string) { + const db = getPlatformDB() + + // use a lock as tenant creation is conflict prone + await locks.doWithLock(tenacyLockOptions, async () => { + const tenants = await getTenants() + + // write the new tenant if it doesn't already exist + if (tenants.tenantIds.indexOf(tenantId) === -1) { + tenants.tenantIds.push(tenantId) + await db.put(tenants) + } + }) +} + +// DELETE + +export async function removeTenant(tenantId: string) { + try { + await locks.doWithLock(tenacyLockOptions, async () => { + const db = getPlatformDB() + const tenants = await getTenants() + tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) + await db.put(tenants) + }) + } catch (err) { + console.error(`Error removing tenant ${tenantId} from info db`, err) + throw err + } +} diff --git a/packages/backend-core/src/platform/tests/tenants.spec.ts b/packages/backend-core/src/platform/tests/tenants.spec.ts new file mode 100644 index 0000000000..92e999cb2d --- /dev/null +++ b/packages/backend-core/src/platform/tests/tenants.spec.ts @@ -0,0 +1,25 @@ +import { DBTestConfiguration, structures } from "../../../tests" +import * as tenants from "../tenants" + +describe("tenants", () => { + const config = new DBTestConfiguration() + + describe("addTenant", () => { + it("concurrently adds multiple tenants safely", async () => { + const tenant1 = structures.tenant.id() + const tenant2 = structures.tenant.id() + const tenant3 = structures.tenant.id() + + await Promise.all([ + tenants.addTenant(tenant1), + tenants.addTenant(tenant2), + tenants.addTenant(tenant3), + ]) + + const tenantIds = await tenants.getTenantIds() + expect(tenantIds.includes(tenant1)).toBe(true) + expect(tenantIds.includes(tenant2)).toBe(true) + expect(tenantIds.includes(tenant3)).toBe(true) + }) + }) +}) diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts new file mode 100644 index 0000000000..c65a7e0ec4 --- /dev/null +++ b/packages/backend-core/src/platform/users.ts @@ -0,0 +1,90 @@ +import { getPlatformDB } from "./platformDb" +import { DEFAULT_TENANT_ID } from "../constants" +import env from "../environment" +import { + PlatformUser, + PlatformUserByEmail, + PlatformUserById, + User, +} from "@budibase/types" + +// READ + +export async function lookupTenantId(userId: string) { + if (!env.MULTI_TENANCY) { + return DEFAULT_TENANT_ID + } + + const user = await getUserDoc(userId) + return user.tenantId +} + +async function getUserDoc(emailOrId: string): Promise { + const db = getPlatformDB() + return db.get(emailOrId) +} + +// CREATE + +function newUserIdDoc(id: string, tenantId: string): PlatformUserById { + return { + _id: id, + tenantId, + } +} + +function newUserEmailDoc( + userId: string, + email: string, + tenantId: string +): PlatformUserByEmail { + return { + _id: email, + userId, + tenantId, + } +} + +/** + * Add a new user id or email doc if it doesn't exist. + */ +async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) { + const db = getPlatformDB() + let user: PlatformUser + + try { + await db.get(emailOrId) + } catch (e: any) { + if (e.status === 404) { + user = newDocFn() + await db.put(user) + } else { + throw e + } + } +} + +export async function addUser(tenantId: string, userId: string, email: string) { + await Promise.all([ + addUserDoc(userId, () => newUserIdDoc(userId, tenantId)), + addUserDoc(email, () => newUserEmailDoc(userId, email, tenantId)), + ]) +} + +// DELETE + +export async function removeUser(user: User) { + const db = getPlatformDB() + const keys = [user._id!, user.email] + const userDocs = await db.allDocs({ + keys, + include_docs: true, + }) + const toDelete = userDocs.rows.map((row: any) => { + return { + ...row.doc, + _deleted: true, + } + }) + await db.bulkDocs(toDelete) +} diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index ea4379f048..5bf2c65c39 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -3,4 +3,4 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" -export * as redlock from "./redlock" +export * as locks from "./redlock" diff --git a/packages/backend-core/src/redis/redlock.ts b/packages/backend-core/src/redis/redlock.ts index 54b2c0a8d1..2021da2b56 100644 --- a/packages/backend-core/src/redis/redlock.ts +++ b/packages/backend-core/src/redis/redlock.ts @@ -1,29 +1,22 @@ import Redlock, { Options } from "redlock" import { getLockClient } from "./init" import { LockOptions, LockType } from "@budibase/types" -import * as tenancy from "../tenancy" - -let noRetryRedlock: Redlock | undefined +import * as context from "../context" +import env from "../environment" const getClient = async (type: LockType): Promise => { + if (env.isTest() && type !== LockType.TRY_ONCE) { + return newRedlock(OPTIONS.TEST) + } switch (type) { case LockType.TRY_ONCE: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.TRY_ONCE) - } - return noRetryRedlock + return newRedlock(OPTIONS.TRY_ONCE) } case LockType.DEFAULT: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.DEFAULT) - } - return noRetryRedlock + return newRedlock(OPTIONS.DEFAULT) } case LockType.DELAY_500: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.DELAY_500) - } - return noRetryRedlock + return newRedlock(OPTIONS.DELAY_500) } default: { throw new Error(`Could not get redlock client: ${type}`) @@ -36,6 +29,11 @@ export const OPTIONS = { // immediately throws an error if the lock is already held retryCount: 0, }, + TEST: { + // higher retry count in unit tests + // due to high contention. + retryCount: 100, + }, DEFAULT: { // the expected clock drift; for more details // see http://redis.io/topics/distlock @@ -69,12 +67,19 @@ export const doWithLock = async (opts: LockOptions, task: any) => { const redlock = await getClient(opts.type) let lock try { - // aquire lock - let name: string = `lock:${tenancy.getTenantId()}_${opts.name}` + // determine lock name + // by default use the tenantId for uniqueness, unless using a system lock + const prefix = opts.systemLock ? "system" : context.getTenantId() + let name: string = `lock:${prefix}_${opts.name}` + + // add additional unique name if required if (opts.nameSuffix) { name = name + `_${opts.nameSuffix}` } + + // create the lock lock = await redlock.lock(name, opts.ttl) + // perform locked task // need to await to ensure completion before unlocking const result = await task() diff --git a/packages/backend-core/src/tenancy/db.ts b/packages/backend-core/src/tenancy/db.ts new file mode 100644 index 0000000000..10477a8579 --- /dev/null +++ b/packages/backend-core/src/tenancy/db.ts @@ -0,0 +1,6 @@ +import { getDB } from "../db/db" +import { getGlobalDBName } from "../context" + +export function getTenantDB(tenantId: string) { + return getDB(getGlobalDBName(tenantId)) +} diff --git a/packages/backend-core/src/tenancy/index.ts b/packages/backend-core/src/tenancy/index.ts index 1618a136dd..3f17e33271 100644 --- a/packages/backend-core/src/tenancy/index.ts +++ b/packages/backend-core/src/tenancy/index.ts @@ -1,2 +1,2 @@ -export * from "../context" +export * from "./db" export * from "./tenancy" diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index 732402bcb7..e8ddf88226 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -1,4 +1,3 @@ -import { doWithDB, getGlobalDBName } from "../db" import { DEFAULT_TENANT_ID, getTenantId, @@ -11,10 +10,7 @@ import { TenantResolutionStrategy, GetTenantIdOptions, } from "@budibase/types" -import { Header, StaticDatabases } from "../constants" - -const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants -const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name +import { Header } from "../constants" export function addTenantToUrl(url: string) { const tenantId = getTenantId() @@ -27,89 +23,6 @@ export function addTenantToUrl(url: string) { return url } -export async function doesTenantExist(tenantId: string) { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - 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 - ) - }) -} - -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) { - 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 - emailDoc.userId = userId - promises.push(db.put(emailDoc)) - } - if (tenants.tenantIds.indexOf(tenantId) === -1) { - tenants.tenantIds.push(tenantId) - promises.push(db.put(tenants)) - await afterCreateTenant() - } - await Promise.all(promises) - }) -} - -export function doWithGlobalDB(tenantId: string, cb: any) { - return doWithDB(getGlobalDBName(tenantId), cb) -} - -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 { - const doc = await db.get(userId) - if (doc && doc.tenantId) { - tenantId = doc.tenantId - } - } catch (err) { - // just return the default - } - return tenantId - }) -} - export const isUserInAppTenant = (appId: string, user?: any) => { let userTenantId if (user) { @@ -121,19 +34,6 @@ export const isUserInAppTenant = (appId: string, user?: any) => { return tenantId === userTenantId } -export async function getTenantIds() { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - let tenants - try { - tenants = await db.get(TENANT_DOC) - } catch (err) { - // if theres an error the doc doesn't exist, no tenants exist - return [] - } - return (tenants && tenants.tenantIds) || [] - }) -} - const ALL_STRATEGIES = Object.values(TenantResolutionStrategy) export const getTenantIDFromCtx = ( diff --git a/packages/backend-core/src/utils/tests/utils.spec.ts b/packages/backend-core/src/utils/tests/utils.spec.ts index b3cd527fb3..7d6c5561e8 100644 --- a/packages/backend-core/src/utils/tests/utils.spec.ts +++ b/packages/backend-core/src/utils/tests/utils.spec.ts @@ -1,21 +1,12 @@ -import { structures } from "../../../tests" +import { structures, DBTestConfiguration } from "../../../tests" import * as utils from "../../utils" -import * as events from "../../events" import * as db from "../../db" import { Header } from "../../constants" -import { doInTenant } from "../../context" import { newid } from "../../utils" +import env from "../../environment" describe("utils", () => { - describe("platformLogout", () => { - it("should call platform logout", async () => { - await doInTenant(structures.tenant.id(), async () => { - const ctx = structures.koa.newContext() - await utils.platformLogout({ ctx, userId: "test" }) - expect(events.auth.logout).toBeCalledTimes(1) - }) - }) - }) + const config = new DBTestConfiguration() describe("getAppIdFromCtx", () => { it("gets appId from header", async () => { @@ -50,21 +41,28 @@ describe("utils", () => { }) it("gets appId from url", async () => { - const ctx = structures.koa.newContext() - const expected = db.generateAppID() - const app = structures.apps.app(expected) + await config.doInTenant(async () => { + const url = "http://test.com" + env._set("PLATFORM_URL", url) - // set custom url - const appUrl = newid() - app.url = `/${appUrl}` - ctx.path = `/app/${appUrl}` + const ctx = structures.koa.newContext() + ctx.host = `${config.tenantId}.test.com` - // save the app - const database = db.getDB(expected) - await database.put(app) + const expected = db.generateAppID(config.tenantId) + const app = structures.apps.app(expected) - const actual = await utils.getAppIdFromCtx(ctx) - expect(actual).toBe(expected) + // set custom url + const appUrl = newid() + app.url = `/${appUrl}` + ctx.path = `/app/${appUrl}` + + // save the app + const database = db.getDB(expected) + await database.put(app) + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(expected) + }) }) it("doesn't get appId from url when previewing", async () => { diff --git a/packages/backend-core/tests/utilities/DBTestConfiguration.ts b/packages/backend-core/tests/utilities/DBTestConfiguration.ts new file mode 100644 index 0000000000..cad62e2979 --- /dev/null +++ b/packages/backend-core/tests/utilities/DBTestConfiguration.ts @@ -0,0 +1,32 @@ +import "./mocks" +import * as structures from "./structures" +import * as testEnv from "./testEnv" +import * as context from "../../src/context" + +class DBTestConfiguration { + tenantId: string + + constructor() { + // db tests need to be multi tenant to prevent conflicts + testEnv.multiTenant() + this.tenantId = structures.tenant.id() + } + + // TENANCY + + doInTenant(task: any) { + return context.doInTenant(this.tenantId, () => { + return task() + }) + } + + getTenantId() { + try { + return context.getTenantId() + } catch (e) { + return this.tenantId! + } + } +} + +export default DBTestConfiguration diff --git a/packages/backend-core/tests/utilities/db.ts b/packages/backend-core/tests/utilities/db.ts deleted file mode 100644 index 84b77bb201..0000000000 --- a/packages/backend-core/tests/utilities/db.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as db from "../../src/db" - -const dbConfig = { - inMemory: true, -} - -export const init = () => { - db.init(dbConfig) -} diff --git a/packages/backend-core/tests/utilities/index.ts b/packages/backend-core/tests/utilities/index.ts index 468d980a7f..efe014908b 100644 --- a/packages/backend-core/tests/utilities/index.ts +++ b/packages/backend-core/tests/utilities/index.ts @@ -4,5 +4,4 @@ export { generator } from "./structures" export * as testEnv from "./testEnv" export * as testContainerUtils from "./testContainerUtils" -import * as dbConfig from "./db" -dbConfig.init() +export { default as DBTestConfiguration } from "./DBTestConfiguration" diff --git a/packages/backend-core/tests/utilities/testEnv.ts b/packages/backend-core/tests/utilities/testEnv.ts index b4f06b5153..b138e019fc 100644 --- a/packages/backend-core/tests/utilities/testEnv.ts +++ b/packages/backend-core/tests/utilities/testEnv.ts @@ -1,12 +1,12 @@ import env from "../../src/environment" -import * as tenancy from "../../src/tenancy" -import { newid } from "../../src/utils" +import * as context from "../../src/context" +import * as structures from "./structures" // TENANCY export async function withTenant(task: (tenantId: string) => any) { - const tenantId = newid() - return tenancy.doInTenant(tenantId, async () => { + const tenantId = structures.tenant.id() + return context.doInTenant(tenantId, async () => { await task(tenantId) }) } @@ -19,6 +19,14 @@ export function multiTenant() { env._set("MULTI_TENANCY", 1) } +export function selfHosted() { + env._set("SELF_HOSTED", 1) +} + +export function cloudHosted() { + env._set("SELF_HOSTED", 0) +} + // NODE export function nodeDev() { diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index d88b1058f9..5f8edb3df6 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -1197,10 +1197,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.1.tgz#2c8b6dc6ff85c33bcd07d0b62cb3d19ddfdb3ab9" - integrity sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ== +"@types/jest@28.1.1": + version "28.1.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-28.1.1.tgz#8c9ba63702a11f8c386ee211280e8b68cb093cd1" + integrity sha512-C2p7yqleUKtCkVjlOur9BWVA4HgUQmEj/HWCt5WzZ5mLXrWnyIfl0wGuArc+kBXsy0ZZfLp+7dywB4HtSVYGVA== dependencies: jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" diff --git a/packages/server/package.json b/packages/server/package.json index 391a5f326f..cfc5fa9fa3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -141,6 +141,7 @@ "@types/pouchdb": "6.4.0", "@types/redis": "4.0.11", "@types/server-destroy": "1.0.1", + "@types/supertest": "2.0.12", "@types/tar": "6.1.3", "@typescript-eslint/parser": "5.45.0", "apidoc": "0.50.4", @@ -159,7 +160,7 @@ "path-to-regexp": "6.2.0", "prettier": "2.5.1", "rimraf": "3.0.2", - "supertest": "4.0.2", + "supertest": "6.2.2", "swagger-jsdoc": "6.1.0", "timekeeper": "2.2.0", "ts-jest": "28.0.4", diff --git a/packages/server/src/api/routes/tests/backup.spec.ts b/packages/server/src/api/routes/tests/backup.spec.ts index 7b325c080d..ef362ef403 100644 --- a/packages/server/src/api/routes/tests/backup.spec.ts +++ b/packages/server/src/api/routes/tests/backup.spec.ts @@ -19,7 +19,6 @@ describe("/backups", () => { .get(`/api/backups/export?appId=${config.getAppId()}&appname=test`) .set(config.defaultHeaders()) .expect(200) - expect(res.text).toBeDefined() expect(res.headers["content-type"]).toEqual("application/gzip") expect(events.app.exported).toBeCalledTimes(1) }) diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 5c45f89a2b..29a5f07607 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -236,42 +236,41 @@ class TestConfiguration { email = this.defaultUserValues.email, roles, }: any = {}) { - return tenancy.doWithGlobalDB(this.getTenantId(), async (db: Database) => { - let existing - try { - existing = await db.get(id) - } catch (err) { - existing = { email } - } - const user = { - _id: id, - ...existing, - roles: roles || {}, - tenantId: this.getTenantId(), - firstName, - lastName, - } - await sessions.createASession(id, { - sessionId: "sessionid", - tenantId: this.getTenantId(), - csrfToken: this.defaultUserValues.csrfToken, - }) - if (builder) { - user.builder = { global: true } - } else { - user.builder = { global: false } - } - if (admin) { - user.admin = { global: true } - } else { - user.admin = { global: false } - } - const resp = await db.put(user) - return { - _rev: resp.rev, - ...user, - } + const db = tenancy.getTenantDB(this.getTenantId()) + let existing + try { + existing = await db.get(id) + } catch (err) { + existing = { email } + } + const user = { + _id: id, + ...existing, + roles: roles || {}, + tenantId: this.getTenantId(), + firstName, + lastName, + } + await sessions.createASession(id, { + sessionId: "sessionid", + tenantId: this.getTenantId(), + csrfToken: this.defaultUserValues.csrfToken, }) + if (builder) { + user.builder = { global: true } + } else { + user.builder = { global: false } + } + if (admin) { + user.admin = { global: true } + } else { + user.admin = { global: false } + } + const resp = await db.put(user) + return { + _rev: resp.rev, + ...user, + } } async createUser( @@ -407,20 +406,19 @@ class TestConfiguration { // API async generateApiKey(userId = this.defaultUserValues.globalUserId) { - return tenancy.doWithGlobalDB(this.getTenantId(), async (db: any) => { - const id = dbCore.generateDevInfoID(userId) - let devInfo - try { - devInfo = await db.get(id) - } catch (err) { - devInfo = { _id: id, userId } - } - devInfo.apiKey = encryption.encrypt( - `${this.getTenantId()}${dbCore.SEPARATOR}${newid()}` - ) - await db.put(devInfo) - return devInfo.apiKey - }) + const db = tenancy.getTenantDB(this.getTenantId()) + const id = dbCore.generateDevInfoID(userId) + let devInfo + try { + devInfo = await db.get(id) + } catch (err) { + devInfo = { _id: id, userId } + } + devInfo.apiKey = encryption.encrypt( + `${this.getTenantId()}${dbCore.SEPARATOR}${newid()}` + ) + await db.put(devInfo) + return devInfo.apiKey } // APP diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 4982b57131..c3fc7b0bbe 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -3547,6 +3547,14 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/superagent@*": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.16.tgz#12c9c16f232f9d89beab91d69368f96ce8e2d881" + integrity sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + "@types/superagent@^4.1.12": version "4.1.15" resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" @@ -3555,6 +3563,13 @@ "@types/cookiejar" "*" "@types/node" "*" +"@types/supertest@2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + "@types/tar@6.1.3": version "6.1.3" resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.3.tgz#46a2ce7617950c4852dfd7e9cd41aa8161b9d750" @@ -4241,7 +4256,7 @@ arrify@^2.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -asap@^2.0.3: +asap@^2.0.0, asap@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== @@ -5416,7 +5431,7 @@ commoner@^0.10.1: q "^1.1.2" recast "^0.11.17" -component-emitter@^1.2.0, component-emitter@^1.2.1: +component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -5518,7 +5533,7 @@ cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookiejar@^2.1.0: +cookiejar@^2.1.3: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== @@ -5967,6 +5982,14 @@ detective@^4.3.1: acorn "^5.2.1" defined "^1.0.0" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-match-patch@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" @@ -6061,11 +6084,6 @@ dotenv@16.0.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== -dotenv@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - dotenv@^8.2.0: version "8.6.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" @@ -6888,7 +6906,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -6977,7 +6995,7 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.1.tgz#790fcff8f808c2e12fabbfb2be5cb2deda448fa0" integrity sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A== -fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8: +fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -7264,7 +7282,7 @@ form-data@4.0.0, form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^2.3.1, form-data@^2.5.0: +form-data@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -7291,11 +7309,21 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^1.1.1, formidable@^1.2.0: +formidable@^1.1.1: version "1.2.6" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== +formidable@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.1.tgz#81269cbea1a613240049f5f61a9d97731517414f" + integrity sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + forwarded-parse@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" @@ -7947,6 +7975,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -10703,7 +10736,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@^1.1.1, methods@^1.1.2: +methods@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== @@ -10755,7 +10788,12 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.24, mime-types@^2.1.27, dependencies: mime-db "1.52.0" -mime@^1.3.4, mime@^1.4.1: +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mime@^1.3.4: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -12502,14 +12540,14 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qs@^6.11.0: +qs@^6.10.3, qs@^6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" -qs@^6.4.0, qs@^6.5.1: +qs@^6.4.0: version "6.10.5" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== @@ -13990,29 +14028,30 @@ sublevel-pouchdb@7.2.2: ltgt "2.2.1" readable-stream "1.1.14" -superagent@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" - integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== +superagent@^7.1.0: + version "7.1.6" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-7.1.6.tgz#64f303ed4e4aba1e9da319f134107a54cacdc9c6" + integrity sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g== dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.2.0" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.3.5" + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.10.3" + readable-stream "^3.6.0" + semver "^7.3.7" -supertest@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36" - integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ== +supertest@6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.2.tgz#04a5998fd3efaff187cb69f07a169755d655b001" + integrity sha512-wCw9WhAtKJsBvh07RaS+/By91NNE0Wh0DN19/hWPlBOU8tAfOtbZoVSV4xXeoKoxgPx0rx2y+y+8660XtE7jzg== dependencies: methods "^1.1.2" - superagent "^3.8.3" + superagent "^7.1.0" supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" diff --git a/packages/types/src/sdk/locks.ts b/packages/types/src/sdk/locks.ts index e6809319b1..d868691891 100644 --- a/packages/types/src/sdk/locks.ts +++ b/packages/types/src/sdk/locks.ts @@ -12,6 +12,7 @@ export enum LockName { MIGRATIONS = "migrations", TRIGGER_QUOTA = "trigger_quota", SYNC_ACCOUNT_LICENSE = "sync_account_license", + UPDATE_TENANTS_DOC = "update_tenants_doc", } export interface LockOptions { diff --git a/packages/worker/package.json b/packages/worker/package.json index 9a55f527df..061a44c958 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -22,7 +22,7 @@ "build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION", "dev:stack:init": "node ./scripts/dev/manage.js init", "dev:builder": "npm run dev:stack:init && nodemon", - "test": "jest --coverage --maxWorkers=2", + "test": "jest --coverage", "test:watch": "jest --watch", "env:multi:enable": "node scripts/multiTenancy.js enable", "env:multi:disable": "node scripts/multiTenancy.js disable", @@ -73,7 +73,7 @@ "@swc/core": "^1.3.25", "@swc/jest": "^0.2.24", "@trendyol/jest-testcontainers": "^2.1.1", - "@types/jest": "26.0.23", + "@types/jest": "28.1.1", "@types/jsonwebtoken": "8.5.1", "@types/koa": "2.13.4", "@types/koa__router": "8.0.8", @@ -81,6 +81,7 @@ "@types/node-fetch": "2.6.1", "@types/pouchdb": "6.4.0", "@types/server-destroy": "1.0.1", + "@types/supertest": "2.0.12", "@types/uuid": "8.3.4", "@typescript-eslint/parser": "5.45.0", "copyfiles": "2.4.1", diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 817480151d..43ec23eade 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,5 +1,5 @@ import { checkInviteCode } from "../../../utilities/redis" -import sdk from "../../../sdk" +import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { BulkUserRequest, @@ -8,6 +8,7 @@ import { CreateAdminUserRequest, InviteUserRequest, InviteUsersRequest, + MigrationType, SearchUsersRequest, User, } from "@budibase/types" @@ -16,7 +17,9 @@ import { cache, errors, events, + migrations, tenancy, + platform, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" @@ -25,7 +28,7 @@ const MAX_USERS_UPLOAD_LIMIT = 1000 export const save = async (ctx: any) => { try { const currentUserId = ctx.user._id - ctx.body = await sdk.users.save(ctx.request.body, { currentUserId }) + ctx.body = await userSdk.save(ctx.request.body, { currentUserId }) } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -35,7 +38,7 @@ const bulkDelete = async (userIds: string[], currentUserId: string) => { if (userIds?.indexOf(currentUserId) !== -1) { throw new Error("Unable to delete self.") } - return await sdk.users.bulkDelete(userIds) + return await userSdk.bulkDelete(userIds) } const bulkCreate = async (users: User[], groupIds: string[]) => { @@ -44,7 +47,7 @@ const bulkCreate = async (users: User[], groupIds: string[]) => { "Max limit for upload is 1000 users. Please reduce file size and try again." ) } - return await sdk.users.bulkCreate(users, groupIds) + return await userSdk.bulkCreate(users, groupIds) } export const bulkUpdate = async (ctx: any) => { @@ -71,16 +74,26 @@ const parseBooleanParam = (param: any) => { export const adminUser = async (ctx: any) => { const { email, password, tenantId } = ctx.request .body as CreateAdminUserRequest + + if (await platform.tenants.exists(tenantId)) { + ctx.throw(403, "Organisation already exists.") + } + + if (env.MULTI_TENANCY) { + // store the new tenant record in the platform db + await platform.tenants.addTenant(tenantId) + await migrations.backPopulateMigrations({ + type: MigrationType.GLOBAL, + tenantId, + }) + } + await tenancy.doInTenant(tenantId, async () => { // account portal sends a pre-hashed password - honour param to prevent double hashing const hashPassword = parseBooleanParam(ctx.request.query.hashPassword) // account portal sends no password for SSO users const requirePassword = parseBooleanParam(ctx.request.query.requirePassword) - if (await tenancy.doesTenantExist(tenantId)) { - ctx.throw(403, "Organisation already exists.") - } - const userExists = await checkAnyUserExists() if (userExists) { ctx.throw( @@ -106,7 +119,7 @@ export const adminUser = async (ctx: any) => { // always bust checklist beforehand, if an error occurs but can proceed, don't get // stuck in a cycle await cache.bustCache(cache.CacheKey.CHECKLIST) - const finalUser = await sdk.users.save(user, { + const finalUser = await userSdk.save(user, { hashPassword, requirePassword, }) @@ -128,7 +141,7 @@ export const adminUser = async (ctx: any) => { export const countByApp = async (ctx: any) => { const appId = ctx.params.appId try { - ctx.body = await sdk.users.countUsersByApp(appId) + ctx.body = await userSdk.countUsersByApp(appId) } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -140,7 +153,7 @@ export const destroy = async (ctx: any) => { ctx.throw(400, "Unable to delete self.") } - await sdk.users.destroy(id, ctx.user) + await userSdk.destroy(id, ctx.user) ctx.body = { message: `User ${id} deleted.`, @@ -149,7 +162,7 @@ export const destroy = async (ctx: any) => { export const search = async (ctx: any) => { const body = ctx.request.body as SearchUsersRequest - const paginated = await sdk.users.paginatedUsers(body) + const paginated = await userSdk.paginatedUsers(body) // user hashed password shouldn't ever be returned for (let user of paginated.data) { if (user) { @@ -161,7 +174,7 @@ export const search = async (ctx: any) => { // called internally by app server user fetch export const fetch = async (ctx: any) => { - const all = await sdk.users.allUsers() + const all = await userSdk.allUsers() // user hashed password shouldn't ever be returned for (let user of all) { if (user) { @@ -173,12 +186,12 @@ export const fetch = async (ctx: any) => { // called internally by app server user find export const find = async (ctx: any) => { - ctx.body = await sdk.users.getUser(ctx.params.id) + ctx.body = await userSdk.getUser(ctx.params.id) } export const tenantUserLookup = async (ctx: any) => { const id = ctx.params.id - const user = await sdk.users.getPlatformUser(id) + const user = await userSdk.getPlatformUser(id) if (user) { ctx.body = user } else { @@ -188,7 +201,7 @@ export const tenantUserLookup = async (ctx: any) => { export const invite = async (ctx: any) => { const request = ctx.request.body as InviteUserRequest - const response = await sdk.users.invite([request]) + const response = await userSdk.invite([request]) // explicitly throw for single user invite if (response.unsuccessful.length) { @@ -207,7 +220,7 @@ export const invite = async (ctx: any) => { export const inviteMultiple = async (ctx: any) => { const request = ctx.request.body as InviteUsersRequest - ctx.body = await sdk.users.invite(request) + ctx.body = await userSdk.invite(request) } export const checkInvite = async (ctx: any) => { @@ -229,7 +242,7 @@ export const inviteAccept = async (ctx: any) => { // info is an extension of the user object that was stored by global const { email, info }: any = await checkInviteCode(inviteCode) ctx.body = await tenancy.doInTenant(info.tenantId, async () => { - const saved = await sdk.users.save({ + const saved = await userSdk.save({ firstName, lastName, password, diff --git a/packages/worker/src/api/controllers/system/tenants.ts b/packages/worker/src/api/controllers/system/tenants.ts index 6916049534..151507358f 100644 --- a/packages/worker/src/api/controllers/system/tenants.ts +++ b/packages/worker/src/api/controllers/system/tenants.ts @@ -1,8 +1,7 @@ -import { BBContext } from "@budibase/types" -import { deprovisioning } from "@budibase/backend-core" -import { quotas } from "@budibase/pro" +import { UserCtx } from "@budibase/types" +import * as tenantSdk from "../../../sdk/tenants" -const _delete = async (ctx: BBContext) => { +export async function destroy(ctx: UserCtx) { const user = ctx.user! const tenantId = ctx.params.tenantId @@ -11,13 +10,10 @@ const _delete = async (ctx: BBContext) => { } try { - await quotas.bustCache() - await deprovisioning.deleteTenant(tenantId) + await tenantSdk.deleteTenant(tenantId) ctx.status = 204 } catch (err) { ctx.log.error(err) throw err } } - -export { _delete as delete } diff --git a/packages/worker/src/api/routes/global/tests/configs.spec.ts b/packages/worker/src/api/routes/global/tests/configs.spec.ts index ee27c4d451..39ad74d295 100644 --- a/packages/worker/src/api/routes/global/tests/configs.spec.ts +++ b/packages/worker/src/api/routes/global/tests/configs.spec.ts @@ -2,7 +2,7 @@ jest.mock("nodemailer") import { TestConfiguration, structures, mocks } from "../../../../tests" mocks.email.mock() -import { Config, context, events } from "@budibase/backend-core" +import { Config, events } from "@budibase/backend-core" describe("configs", () => { const config = new TestConfiguration() @@ -113,64 +113,56 @@ describe("configs", () => { describe("create", () => { it("should create activated OIDC config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await saveOIDCConfig() - expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) - expect(events.auth.SSODeactivated).not.toBeCalled() - expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) - }) + await saveOIDCConfig() + expect(events.auth.SSOCreated).toBeCalledTimes(1) + expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSODeactivated).not.toBeCalled() + expect(events.auth.SSOActivated).toBeCalledTimes(1) + expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) + await config.deleteConfig(Config.OIDC) }) it("should create deactivated OIDC config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await saveOIDCConfig({ activated: false }) - expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) - expect(events.auth.SSOActivated).not.toBeCalled() - expect(events.auth.SSODeactivated).not.toBeCalled() - await config.deleteConfig(Config.OIDC) - }) + await saveOIDCConfig({ activated: false }) + expect(events.auth.SSOCreated).toBeCalledTimes(1) + expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOActivated).not.toBeCalled() + expect(events.auth.SSODeactivated).not.toBeCalled() + await config.deleteConfig(Config.OIDC) }) }) describe("update", () => { it("should update OIDC config to deactivated", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const oidcConf = await saveOIDCConfig() - jest.clearAllMocks() - await saveOIDCConfig( - { ...oidcConf.config.configs[0], activated: false }, - oidcConf._id, - oidcConf._rev - ) - expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) - expect(events.auth.SSOActivated).not.toBeCalled() - expect(events.auth.SSODeactivated).toBeCalledTimes(1) - expect(events.auth.SSODeactivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) - }) + const oidcConf = await saveOIDCConfig() + jest.clearAllMocks() + await saveOIDCConfig( + { ...oidcConf.config.configs[0], activated: false }, + oidcConf._id, + oidcConf._rev + ) + expect(events.auth.SSOUpdated).toBeCalledTimes(1) + expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOActivated).not.toBeCalled() + expect(events.auth.SSODeactivated).toBeCalledTimes(1) + expect(events.auth.SSODeactivated).toBeCalledWith(Config.OIDC) + await config.deleteConfig(Config.OIDC) }) it("should update OIDC config to activated", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const oidcConf = await saveOIDCConfig({ activated: false }) - jest.clearAllMocks() - await saveOIDCConfig( - { ...oidcConf.config.configs[0], activated: true }, - oidcConf._id, - oidcConf._rev - ) - expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) - expect(events.auth.SSODeactivated).not.toBeCalled() - expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) - }) + const oidcConf = await saveOIDCConfig({ activated: false }) + jest.clearAllMocks() + await saveOIDCConfig( + { ...oidcConf.config.configs[0], activated: true }, + oidcConf._id, + oidcConf._rev + ) + expect(events.auth.SSOUpdated).toBeCalledTimes(1) + expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSODeactivated).not.toBeCalled() + expect(events.auth.SSOActivated).toBeCalledTimes(1) + expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) + await config.deleteConfig(Config.OIDC) }) }) }) @@ -187,26 +179,22 @@ describe("configs", () => { describe("create", () => { it("should create SMTP config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await config.deleteConfig(Config.SMTP) - await saveSMTPConfig() - expect(events.email.SMTPUpdated).not.toBeCalled() - expect(events.email.SMTPCreated).toBeCalledTimes(1) - await config.deleteConfig(Config.SMTP) - }) + await config.deleteConfig(Config.SMTP) + await saveSMTPConfig() + expect(events.email.SMTPUpdated).not.toBeCalled() + expect(events.email.SMTPCreated).toBeCalledTimes(1) + await config.deleteConfig(Config.SMTP) }) }) describe("update", () => { it("should update SMTP config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const smtpConf = await saveSMTPConfig() - jest.clearAllMocks() - await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev) - expect(events.email.SMTPCreated).not.toBeCalled() - expect(events.email.SMTPUpdated).toBeCalledTimes(1) - await config.deleteConfig(Config.SMTP) - }) + const smtpConf = await saveSMTPConfig() + jest.clearAllMocks() + await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev) + expect(events.email.SMTPCreated).not.toBeCalled() + expect(events.email.SMTPUpdated).toBeCalledTimes(1) + await config.deleteConfig(Config.SMTP) }) }) }) @@ -223,73 +211,65 @@ describe("configs", () => { describe("create", () => { it("should create settings config with default settings", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await config.deleteConfig(Config.SETTINGS) + await config.deleteConfig(Config.SETTINGS) - await saveSettingsConfig() + await saveSettingsConfig() - expect(events.org.nameUpdated).not.toBeCalled() - expect(events.org.logoUpdated).not.toBeCalled() - expect(events.org.platformURLUpdated).not.toBeCalled() - }) + expect(events.org.nameUpdated).not.toBeCalled() + expect(events.org.logoUpdated).not.toBeCalled() + expect(events.org.platformURLUpdated).not.toBeCalled() }) it("should create settings config with non-default settings", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - config.modeSelf() - await config.deleteConfig(Config.SETTINGS) - const conf = { - company: "acme", - logoUrl: "http://example.com", - platformUrl: "http://example.com", - } + config.selfHosted() + await config.deleteConfig(Config.SETTINGS) + const conf = { + company: "acme", + logoUrl: "http://example.com", + platformUrl: "http://example.com", + } - await saveSettingsConfig(conf) + await saveSettingsConfig(conf) - expect(events.org.nameUpdated).toBeCalledTimes(1) - expect(events.org.logoUpdated).toBeCalledTimes(1) - expect(events.org.platformURLUpdated).toBeCalledTimes(1) - config.modeCloud() - }) + expect(events.org.nameUpdated).toBeCalledTimes(1) + expect(events.org.logoUpdated).toBeCalledTimes(1) + expect(events.org.platformURLUpdated).toBeCalledTimes(1) + config.cloudHosted() }) }) describe("update", () => { it("should update settings config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - config.modeSelf() - await config.deleteConfig(Config.SETTINGS) - const settingsConfig = await saveSettingsConfig() - settingsConfig.config.company = "acme" - settingsConfig.config.logoUrl = "http://example.com" - settingsConfig.config.platformUrl = "http://example.com" + config.selfHosted() + await config.deleteConfig(Config.SETTINGS) + const settingsConfig = await saveSettingsConfig() + settingsConfig.config.company = "acme" + settingsConfig.config.logoUrl = "http://example.com" + settingsConfig.config.platformUrl = "http://example.com" - await saveSettingsConfig( - settingsConfig.config, - settingsConfig._id, - settingsConfig._rev - ) + await saveSettingsConfig( + settingsConfig.config, + settingsConfig._id, + settingsConfig._rev + ) - expect(events.org.nameUpdated).toBeCalledTimes(1) - expect(events.org.logoUpdated).toBeCalledTimes(1) - expect(events.org.platformURLUpdated).toBeCalledTimes(1) - config.modeCloud() - }) + expect(events.org.nameUpdated).toBeCalledTimes(1) + expect(events.org.logoUpdated).toBeCalledTimes(1) + expect(events.org.platformURLUpdated).toBeCalledTimes(1) + config.cloudHosted() }) }) }) }) it("should return the correct checklist status based on the state of the budibase installation", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await config.saveSmtpConfig() + await config.saveSmtpConfig() - const res = await config.api.configs.getConfigChecklist() - const checklist = res.body + const res = await config.api.configs.getConfigChecklist() + const checklist = res.body - expect(checklist.apps.checked).toBeFalsy() - expect(checklist.smtp.checked).toBeTruthy() - expect(checklist.adminUser.checked).toBeTruthy() - }) + expect(checklist.apps.checked).toBeFalsy() + expect(checklist.smtp.checked).toBeTruthy() + expect(checklist.adminUser.checked).toBeTruthy() }) }) diff --git a/packages/worker/src/api/routes/global/tests/roles.spec.ts b/packages/worker/src/api/routes/global/tests/roles.spec.ts index 622a643f25..477ccaf94c 100644 --- a/packages/worker/src/api/routes/global/tests/roles.spec.ts +++ b/packages/worker/src/api/routes/global/tests/roles.spec.ts @@ -32,6 +32,7 @@ async function addAppMetadata() { describe("/api/global/roles", () => { const config = new TestConfiguration() + const role = new roles.Role( db.generateRoleID("newRole"), roles.BUILTIN_ROLE_IDS.BASIC, @@ -43,13 +44,13 @@ describe("/api/global/roles", () => { }) beforeEach(async () => { - appId = db.generateAppID() + appId = db.generateAppID(config.tenantId) appDb = db.getDB(appId) const mockAppDB = context.getAppDB as Mock mockAppDB.mockReturnValue(appDb) await addAppMetadata() - appDb.put(role) + await appDb.put(role) }) afterAll(async () => { diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index a8b07ec815..31ef1d9b0c 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -3,7 +3,9 @@ import { InviteUsersResponse, User } from "@budibase/types" jest.mock("nodemailer") import { TestConfiguration, mocks, structures } from "../../../../tests" const sendMailMock = mocks.email.mock() -import { context, events, tenancy } from "@budibase/backend-core" +import { events, tenancy, accounts as _accounts } from "@budibase/backend-core" + +const accounts = jest.mocked(_accounts) describe("/api/global/users", () => { const config = new TestConfiguration() @@ -20,26 +22,24 @@ describe("/api/global/users", () => { jest.clearAllMocks() }) - describe("invite", () => { + describe("POST /api/global/users/invite", () => { it("should be able to generate an invitation", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const email = structures.users.newEmail() - const { code, res } = await config.api.users.sendUserInvite( - sendMailMock, - email - ) + const email = structures.users.newEmail() + const { code, res } = await config.api.users.sendUserInvite( + sendMailMock, + email + ) - expect(res.body).toEqual({ message: "Invitation has been sent." }) - expect(sendMailMock).toHaveBeenCalled() - expect(code).toBeDefined() - expect(events.user.invited).toBeCalledTimes(1) - }) + expect(res.body).toEqual({ message: "Invitation has been sent." }) + expect(sendMailMock).toHaveBeenCalled() + expect(code).toBeDefined() + expect(events.user.invited).toBeCalledTimes(1) }) it("should not be able to generate an invitation for existing user", async () => { const { code, res } = await config.api.users.sendUserInvite( sendMailMock, - config.defaultUser!.email, + config.user!.email, 400 ) @@ -50,26 +50,24 @@ describe("/api/global/users", () => { }) it("should be able to create new user from invite", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const email = structures.users.newEmail() - const { code } = await config.api.users.sendUserInvite( - sendMailMock, - email - ) + const email = structures.users.newEmail() + const { code } = await config.api.users.sendUserInvite( + sendMailMock, + email + ) - const res = await config.api.users.acceptInvite(code) + const res = await config.api.users.acceptInvite(code) - expect(res.body._id).toBeDefined() - const user = await config.getUser(email) - expect(user).toBeDefined() - expect(user._id).toEqual(res.body._id) - expect(events.user.inviteAccepted).toBeCalledTimes(1) - expect(events.user.inviteAccepted).toBeCalledWith(user) - }) + expect(res.body._id).toBeDefined() + const user = await config.getUser(email) + expect(user).toBeDefined() + expect(user._id).toEqual(res.body._id) + expect(events.user.inviteAccepted).toBeCalledTimes(1) + expect(events.user.inviteAccepted).toBeCalledWith(user) }) }) - describe("inviteMultiple", () => { + describe("POST /api/global/users/multi/invite", () => { it("should be able to generate an invitation", async () => { const newUserInvite = () => ({ email: structures.users.newEmail(), @@ -87,7 +85,7 @@ describe("/api/global/users", () => { }) it("should not be able to generate an invitation for existing user", async () => { - const request = [{ email: config.defaultUser!.email, userInfo: {} }] + const request = [{ email: config.user!.email, userInfo: {} }] const res = await config.api.users.sendMultiUserInvite(request) @@ -100,7 +98,7 @@ describe("/api/global/users", () => { }) }) - describe("bulk (create)", () => { + describe("POST /api/global/users/bulk", () => { it("should ignore users existing in the same tenant", async () => { const user = await config.createUser() jest.clearAllMocks() @@ -163,7 +161,7 @@ describe("/api/global/users", () => { }) }) - describe("create", () => { + describe("POST /api/global/users", () => { it("should be able to create a basic user", async () => { const user = structures.users.user() @@ -242,7 +240,7 @@ describe("/api/global/users", () => { it("should not be able to create user with the same email as an account", async () => { const user = structures.users.user() const account = structures.accounts.cloudAccount() - mocks.accounts.getAccount.mockReturnValueOnce(account) + accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.saveUser(user, 400) @@ -283,7 +281,7 @@ describe("/api/global/users", () => { }) }) - describe("update", () => { + describe("POST /api/global/users (update)", () => { it("should be able to update a basic user", async () => { const user = await config.createUser() jest.clearAllMocks() @@ -298,7 +296,7 @@ describe("/api/global/users", () => { }) it("should not allow a user to update their own admin/builder status", async () => { - const user = (await config.api.users.getUser(config.defaultUser?._id!)) + const user = (await config.api.users.getUser(config.user?._id!)) .body as User await config.api.users.saveUser({ ...user, @@ -468,9 +466,9 @@ describe("/api/global/users", () => { }) }) - describe("bulk (delete)", () => { + describe("POST /api/global/users/bulk (delete)", () => { it("should not be able to bulk delete current user", async () => { - const user = await config.defaultUser! + const user = await config.user! const response = await config.api.users.bulkDeleteUsers([user._id!], 400) @@ -482,7 +480,7 @@ describe("/api/global/users", () => { const user = await config.createUser() const account = structures.accounts.cloudAccount() account.budibaseUserId = user._id! - mocks.accounts.getAccountByTenantId.mockReturnValue(account) + accounts.getAccountByTenantId.mockReturnValue(Promise.resolve(account)) const response = await config.api.users.bulkDeleteUsers([user._id!]) @@ -497,7 +495,7 @@ describe("/api/global/users", () => { it("should be able to bulk delete users", async () => { const account = structures.accounts.cloudAccount() - mocks.accounts.getAccountByTenantId.mockReturnValue(account) + accounts.getAccountByTenantId.mockReturnValue(Promise.resolve(account)) const builder = structures.users.builderUser() const admin = structures.users.adminUser() @@ -521,7 +519,7 @@ describe("/api/global/users", () => { }) }) - describe("destroy", () => { + describe("DELETE /api/global/users/:userId", () => { it("should be able to destroy a basic user", async () => { const user = await config.createUser() jest.clearAllMocks() @@ -558,7 +556,7 @@ describe("/api/global/users", () => { it("should not be able to destroy account owner", async () => { const user = await config.createUser() const account = structures.accounts.cloudAccount() - mocks.accounts.getAccount.mockReturnValueOnce(account) + accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.deleteUser(user._id!, 400) @@ -566,10 +564,10 @@ describe("/api/global/users", () => { }) it("should not be able to destroy account owner as account owner", async () => { - const user = await config.defaultUser! + const user = await config.user! const account = structures.accounts.cloudAccount() account.email = user.email - mocks.accounts.getAccount.mockReturnValueOnce(account) + accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.deleteUser(user._id!, 400) diff --git a/packages/worker/src/api/routes/system/tenants.ts b/packages/worker/src/api/routes/system/tenants.ts index 111cfc5819..234459cfdc 100644 --- a/packages/worker/src/api/routes/system/tenants.ts +++ b/packages/worker/src/api/routes/system/tenants.ts @@ -7,7 +7,7 @@ const router: Router = new Router() router.delete( "/api/system/tenants/:tenantId", middleware.adminOnly, - controller.delete + controller.destroy ) export default router diff --git a/packages/worker/src/api/routes/system/tests/restore.spec.ts b/packages/worker/src/api/routes/system/tests/restore.spec.ts index 4dd973270f..2130fd8dde 100644 --- a/packages/worker/src/api/routes/system/tests/restore.spec.ts +++ b/packages/worker/src/api/routes/system/tests/restore.spec.ts @@ -25,12 +25,12 @@ describe("/api/system/restore", () => { }) it("restores in self host", async () => { - config.modeSelf() + config.selfHosted() const res = await config.api.restore.restored() expect(res.body).toEqual({ message: "System prepared after restore.", }) - config.modeCloud() + config.cloudHosted() }) }) }) diff --git a/packages/worker/src/api/routes/system/tests/status.spec.ts b/packages/worker/src/api/routes/system/tests/status.spec.ts index afd3f8ac46..fe0ff13551 100644 --- a/packages/worker/src/api/routes/system/tests/status.spec.ts +++ b/packages/worker/src/api/routes/system/tests/status.spec.ts @@ -1,6 +1,6 @@ import { TestConfiguration } from "../../../../tests" -import { accounts } from "@budibase/backend-core" -import { mocks } from "@budibase/backend-core/tests" +import { accounts as _accounts } from "@budibase/backend-core" +const accounts = jest.mocked(_accounts) describe("/api/system/status", () => { const config = new TestConfiguration() @@ -19,7 +19,7 @@ describe("/api/system/status", () => { describe("GET /api/system/status", () => { it("returns status in self host", async () => { - config.modeSelf() + config.selfHosted() const res = await config.api.status.getStatus() expect(res.body).toEqual({ health: { @@ -27,7 +27,7 @@ describe("/api/system/status", () => { }, }) expect(accounts.getStatus).toBeCalledTimes(0) - config.modeCloud() + config.cloudHosted() }) it("returns status in cloud", async () => { @@ -37,7 +37,7 @@ describe("/api/system/status", () => { }, } - mocks.accounts.getStatus.mockReturnValueOnce(value) + accounts.getStatus.mockReturnValueOnce(Promise.resolve(value)) const res = await config.api.status.getStatus() diff --git a/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts b/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts index 941791fe93..1ffcc762c4 100644 --- a/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts +++ b/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts @@ -1,5 +1,6 @@ import { User } from "@budibase/types" -import sdk from "../../sdk" +import * as usersSdk from "../../sdk/users" +import { platform } from "@budibase/backend-core" /** * Date: @@ -9,11 +10,11 @@ import sdk from "../../sdk" * Re-sync the global-db users to the global-info db users */ export const run = async (globalDb: any) => { - const users = (await sdk.users.allUsers()) as User[] + const users = (await usersSdk.allUsers()) as User[] const promises = [] for (let user of users) { promises.push( - sdk.users.addTenant(user.tenantId, user._id as string, user.email) + platform.users.addUser(user.tenantId, user._id as string, user.email) ) } await Promise.all(promises) diff --git a/packages/worker/src/sdk/tenants/index.ts b/packages/worker/src/sdk/tenants/index.ts new file mode 100644 index 0000000000..19b7ea6615 --- /dev/null +++ b/packages/worker/src/sdk/tenants/index.ts @@ -0,0 +1 @@ +export * from "./tenants" diff --git a/packages/worker/src/sdk/tenants/tenants.ts b/packages/worker/src/sdk/tenants/tenants.ts new file mode 100644 index 0000000000..3ba9c3f3a7 --- /dev/null +++ b/packages/worker/src/sdk/tenants/tenants.ts @@ -0,0 +1,76 @@ +import { App } from "@budibase/types" +import { tenancy, db as dbCore, platform } from "@budibase/backend-core" +import { quotas } from "@budibase/pro" + +export async function deleteTenant(tenantId: string) { + await quotas.bustCache() + await platform.tenants.removeTenant(tenantId) + await removeGlobalDB(tenantId) + await removeTenantUsers(tenantId) + await removeTenantApps(tenantId) +} + +async function removeGlobalDB(tenantId: string) { + try { + const db = tenancy.getTenantDB(tenantId) + await db.destroy() + } catch (err) { + console.error(`Error removing tenant ${tenantId} users from info db`, err) + throw err + } +} + +async function removeTenantApps(tenantId: string) { + try { + const apps = (await dbCore.getAllApps({ all: true })) as App[] + const destroyPromises = apps.map(app => { + const db = dbCore.getDB(app.appId) + return db.destroy() + }) + await Promise.allSettled(destroyPromises) + } catch (err) { + console.error(`Error removing tenant ${tenantId} apps`, err) + throw err + } +} + +function getTenantUsers(tenantId: string) { + const db = tenancy.getTenantDB(tenantId) + + return db.allDocs( + dbCore.getGlobalUserParams(null, { + include_docs: true, + }) + ) +} + +async function removeTenantUsers(tenantId: string) { + try { + const allUsers = await getTenantUsers(tenantId) + const allEmails = allUsers.rows.map((row: any) => row.doc.email) + + // get the id and email doc ids + let keys = allUsers.rows.map((row: any) => row.id) + keys = keys.concat(allEmails) + + const platformDb = platform.getPlatformDB() + + // retrieve the docs + const userDocs = await platformDb.allDocs({ + keys, + include_docs: true, + }) + + // delete the docs + const toDelete = userDocs.rows.map((row: any) => { + return { + ...row.doc, + _deleted: true, + } + }) + await platformDb.bulkDocs(toDelete) + } catch (err) { + console.error(`Error removing tenant ${tenantId} users from info db`, err) + throw err + } +} diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 8410d0b2e0..330cdfde6d 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -549,7 +549,7 @@ export const bulkDelete = async ( export const destroy = async (id: string, currentUser: any) => { const db = tenancy.getGlobalDB() - const dbUser = await db.get(id) + const dbUser = (await db.get(id)) as User const userId = dbUser._id as string if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 0cc1e61e65..7d075e7fef 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -15,61 +15,29 @@ const supertest = require("supertest") import { Config } from "../constants" import { users, - tenancy, + context, sessions, auth, constants, env as coreEnv, - DEFAULT_TENANT_ID, } from "@budibase/backend-core" -import structures, { TENANT_ID, CSRF_TOKEN } from "./structures" +import structures, { CSRF_TOKEN } from "./structures" import { CreateUserResponse, User, AuthToken } from "@budibase/types" import API from "./api" -import sdk from "../sdk" - -enum Mode { - CLOUD = "cloud", - SELF = "self", -} - -async function retry any>( - fn: T, - maxTry: number = 5, - retryCount = 1 -): Promise>> { - const currRetry = typeof retryCount === "number" ? retryCount : 1 - try { - const result = await fn() - return result - } catch (e) { - console.log(`Retry ${currRetry} failed.`) - if (currRetry > maxTry) { - console.log(`All ${maxTry} retry attempts exhausted`) - throw e - } - return retry(fn, maxTry, currRetry + 1) - } -} class TestConfiguration { server: any request: any api: API - defaultUser?: User - tenant1User?: User - #tenantId?: string + tenantId: string + user?: User + userPassword = "test" - constructor( - opts: { openServer: boolean; mode: Mode } = { - openServer: true, - mode: Mode.CLOUD, - } - ) { - if (opts.mode === Mode.CLOUD) { - this.modeCloud() - } else if (opts.mode === Mode.SELF) { - this.modeSelf() - } + constructor(opts: { openServer: boolean } = { openServer: true }) { + // default to cloud hosting + this.cloudHosted() + + this.tenantId = structures.tenant.id() if (opts.openServer) { env.PORT = "0" // random port @@ -85,26 +53,19 @@ class TestConfiguration { return this.request } - // MODES - - setMultiTenancy = (value: boolean) => { - env._set("MULTI_TENANCY", value) - coreEnv._set("MULTI_TENANCY", value) - } + // HOSTING setSelfHosted = (value: boolean) => { env._set("SELF_HOSTED", value) coreEnv._set("SELF_HOSTED", value) } - modeCloud = () => { + cloudHosted = () => { this.setSelfHosted(false) - this.setMultiTenancy(true) } - modeSelf = () => { + selfHosted = () => { this.setSelfHosted(true) - this.setMultiTenancy(false) } // UTILS @@ -125,7 +86,7 @@ class TestConfiguration { if (params) { request.params = params } - await tenancy.doInTenant(this.getTenantId(), () => { + await context.doInTenant(this.getTenantId(), () => { return controlFunc(request) }) return request.body @@ -135,18 +96,10 @@ class TestConfiguration { async beforeAll() { try { - this.#tenantId = structures.tenant.id() - - // Running tests in parallel causes issues creating the globaldb twice. This ensures the db is properly created before starting - await retry(async () => await this.createDefaultUser()) - await this.createSession(this.defaultUser!) - - await tenancy.doInTenant(this.#tenantId, async () => { - await this.createTenant1User() - await this.createSession(this.tenant1User!) - }) + await this.createDefaultUser() + await this.createSession(this.user!) } catch (e: any) { - console.log(e) + console.error(e) throw new Error(e.message) } } @@ -159,12 +112,16 @@ class TestConfiguration { // TENANCY + doInTenant(task: any) { + return context.doInTenant(this.tenantId, () => { + return task() + }) + } + createTenant = async (): Promise => { // create user / new tenant const res = await this.api.users.createAdminUser() - await sdk.users.addTenant(res.tenantId, res.userId, res.email) - // return the created user const userRes = await this.api.users.getUser(res.userId, { headers: { @@ -182,9 +139,9 @@ class TestConfiguration { getTenantId() { try { - return tenancy.getTenantId() - } catch (e: any) { - return DEFAULT_TENANT_ID + return context.getTenantId() + } catch (e) { + return this.tenantId! } } @@ -232,14 +189,11 @@ class TestConfiguration { } defaultHeaders() { - const tenantId = this.getTenantId() - if (tenantId === TENANT_ID) { - return this.authHeaders(this.defaultUser!) - } else if (tenantId === this.getTenantId()) { - return this.authHeaders(this.tenant1User!) - } else { - throw new Error("could not determine auth headers to use") - } + return this.authHeaders(this.user!) + } + + tenantIdHeaders() { + return { [constants.Header.TENANT_ID]: this.tenantId } } internalAPIHeaders() { @@ -254,20 +208,15 @@ class TestConfiguration { async createDefaultUser() { const user = structures.users.adminUser({ - password: "test", + password: this.userPassword, }) - this.defaultUser = await this.createUser(user) - } - - async createTenant1User() { - const user = structures.users.adminUser({ - password: "test", + await context.doInTenant(this.tenantId!, async () => { + this.user = await this.createUser(user) }) - this.tenant1User = await this.createUser(user) } async getUser(email: string): Promise { - return tenancy.doInTenant(this.getTenantId(), () => { + return context.doInTenant(this.getTenantId(), () => { return users.getGlobalUserByEmail(email) }) } diff --git a/packages/worker/src/tests/api/base.ts b/packages/worker/src/tests/api/base.ts index c1263ed5cb..460d61e70a 100644 --- a/packages/worker/src/tests/api/base.ts +++ b/packages/worker/src/tests/api/base.ts @@ -1,4 +1,5 @@ import TestConfiguration from "../TestConfiguration" +import { SuperTest, Test } from "supertest" export interface TestAPIOpts { headers?: any @@ -7,7 +8,7 @@ export interface TestAPIOpts { export abstract class TestAPI { config: TestConfiguration - request: any + request: SuperTest protected constructor(config: TestConfiguration) { this.config = config diff --git a/packages/worker/src/tests/api/restore.ts b/packages/worker/src/tests/api/restore.ts index 6069c20185..c6a646317d 100644 --- a/packages/worker/src/tests/api/restore.ts +++ b/packages/worker/src/tests/api/restore.ts @@ -9,6 +9,7 @@ export class RestoreAPI extends TestAPI { restored = (opts?: TestAPIOpts) => { return this.request .post(`/api/system/restored`) + .set(this.config.tenantIdHeaders()) .expect(opts?.status ? opts.status : 200) } } diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index e7a02fce1f..b03e9d9d7c 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -800,17 +800,6 @@ slash "^3.0.0" write-file-atomic "^4.0.1" -"@jest/types@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" - integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^15.0.0" - chalk "^4.0.0" - "@jest/types@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" @@ -1317,6 +1306,11 @@ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3" integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA== +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/cookies@*": version "0.7.7" resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81" @@ -1413,13 +1407,13 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@26.0.23": - version "26.0.23" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7" - integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA== +"@types/jest@28.1.1": + version "28.1.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-28.1.1.tgz#8c9ba63702a11f8c386ee211280e8b68cb093cd1" + integrity sha512-C2p7yqleUKtCkVjlOur9BWVA4HgUQmEj/HWCt5WzZ5mLXrWnyIfl0wGuArc+kBXsy0ZZfLp+7dywB4HtSVYGVA== dependencies: - jest-diff "^26.0.0" - pretty-format "^26.0.0" + jest-matcher-utils "^27.0.0" + pretty-format "^27.0.0" "@types/json-buffer@~3.0.0": version "3.0.0" @@ -1689,6 +1683,21 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/superagent@*": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.16.tgz#12c9c16f232f9d89beab91d69368f96ce8e2d881" + integrity sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + "@types/tough-cookie@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" @@ -1704,13 +1713,6 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== -"@types/yargs@^15.0.0": - version "15.0.14" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" - integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== - dependencies: - "@types/yargs-parser" "*" - "@types/yargs@^16.0.0": version "16.0.5" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3" @@ -1898,7 +1900,7 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== -ansi-regex@^5.0.0, ansi-regex@^5.0.1: +ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -2999,10 +3001,10 @@ dezalgo@1.0.3: asap "^2.0.0" wrappy "1" -diff-sequences@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" - integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== diff-sequences@^28.1.1: version "28.1.1" @@ -3066,11 +3068,6 @@ dotenv@16.0.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== -dotenv@8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" - integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== - double-ended-queue@2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" @@ -4727,15 +4724,15 @@ jest-config@^28.1.3: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^26.0.0: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" - integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: chalk "^4.0.0" - diff-sequences "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" jest-diff@^28.1.3: version "28.1.3" @@ -4777,10 +4774,10 @@ jest-environment-node@^28.1.3: jest-mock "^28.1.3" jest-util "^28.1.3" -jest-get-type@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" - integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== jest-get-type@^28.0.2: version "28.0.2" @@ -4814,6 +4811,16 @@ jest-leak-detector@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" +jest-matcher-utils@^27.0.0: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== + dependencies: + chalk "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + jest-matcher-utils@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz#5a77f1c129dd5ba3b4d7fc20728806c78893146e" @@ -6624,14 +6631,13 @@ prettier@2.3.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== -pretty-format@^26.0.0, pretty-format@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" - integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== +pretty-format@^27.0.0, pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== dependencies: - "@jest/types" "^26.6.2" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" react-is "^17.0.1" pretty-format@^28.1.3: From 07e5598538f745b8495e5f58d7920c2d1a22fc38 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 12:09:16 +0000 Subject: [PATCH 03/12] Enable use of redis container in worker tests --- packages/backend-core/src/environment.ts | 5 +++-- packages/backend-core/src/queue/queue.ts | 2 +- packages/backend-core/src/redis/redis.ts | 13 +++++++++---- packages/backend-core/src/redis/utils.ts | 10 ++++------ .../tests/utilities/testContainerUtils.ts | 5 +++++ 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index d742ca1cc9..95e636c9b4 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -44,8 +44,9 @@ const environment = { GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, SALT_ROUNDS: process.env.SALT_ROUNDS, - REDIS_URL: process.env.REDIS_URL, - REDIS_PASSWORD: process.env.REDIS_PASSWORD, + REDIS_URL: process.env.REDIS_URL || "localhost:6379", + REDIS_PASSWORD: process.env.REDIS_PASSWORD || "budibase", + MOCK_REDIS: process.env.MOCK_REDIS, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, AWS_REGION: process.env.AWS_REGION, diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index b34d46e463..8e1fc1fbf3 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -4,7 +4,6 @@ import { JobQueue } from "./constants" import InMemoryQueue from "./inMemoryQueue" import BullQueue from "bull" import { addListeners, StalledFn } from "./listeners" -const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const CLEANUP_PERIOD_MS = 60 * 1000 let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = [] @@ -20,6 +19,7 @@ export function createQueue( jobQueue: JobQueue, opts: { removeStalledCb?: StalledFn } = {} ): BullQueue.Queue { + const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const queueConfig: any = redisProtocolUrl || { redis: redisOpts } let queue: any if (!env.isTest()) { diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 0267709cdc..2669cd816a 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -1,6 +1,6 @@ import env from "../environment" // ioredis mock is all in memory -const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") +const Redis = env.MOCK_REDIS ? require("ioredis-mock") : require("ioredis") import { addDbPrefix, removeDbPrefix, @@ -17,8 +17,13 @@ const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT // for testing just generate the client once let CLOSED = false let CLIENTS: { [key: number]: any } = {} -// if in test always connected -let CONNECTED = env.isTest() + +let CONNECTED = false + +// mock redis always connected +if (env.MOCK_REDIS) { + CONNECTED = true +} function pickClient(selectDb: number): any { return CLIENTS[selectDb] @@ -57,7 +62,7 @@ function init(selectDb = DEFAULT_SELECT_DB) { return } // testing uses a single in memory client - if (env.isTest()) { + if (env.MOCK_REDIS) { CLIENTS[selectDb] = new Redis(getRedisOptions()) } // start the timer - only allowed 5 seconds to connect diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts index 4c556ebd54..7606c77b87 100644 --- a/packages/backend-core/src/redis/utils.ts +++ b/packages/backend-core/src/redis/utils.ts @@ -2,8 +2,6 @@ import env from "../environment" const SLOT_REFRESH_MS = 2000 const CONNECT_TIMEOUT_MS = 10000 -const REDIS_URL = !env.REDIS_URL ? "localhost:6379" : env.REDIS_URL -const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD export const SEPARATOR = "-" /** @@ -60,8 +58,8 @@ export enum SelectableDatabase { } export function getRedisOptions(clustered = false) { - let password = REDIS_PASSWORD - let url: string[] | string = REDIS_URL.split("//") + let password = env.REDIS_PASSWORD + let url: string[] | string = env.REDIS_URL.split("//") // get rid of the protocol url = url.length > 1 ? url[1] : url[0] // check for a password etc @@ -78,8 +76,8 @@ export function getRedisOptions(clustered = false) { let redisProtocolUrl // fully qualified redis URL - if (/rediss?:\/\//.test(REDIS_URL)) { - redisProtocolUrl = REDIS_URL + if (/rediss?:\/\//.test(env.REDIS_URL)) { + redisProtocolUrl = env.REDIS_URL } const opts: any = { diff --git a/packages/backend-core/tests/utilities/testContainerUtils.ts b/packages/backend-core/tests/utilities/testContainerUtils.ts index 22198bd496..11c5fca806 100644 --- a/packages/backend-core/tests/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/utilities/testContainerUtils.ts @@ -34,12 +34,17 @@ function getMinioConfig() { return getContainerInfo("minio-service", 9000) } +function getRedisConfig() { + return getContainerInfo("redis-service", 6379) +} + export function setupEnv(...envs: any[]) { const configs = [ { key: "COUCH_DB_PORT", value: getCouchConfig().port }, { key: "COUCH_DB_URL", value: getCouchConfig().url }, { key: "MINIO_PORT", value: getMinioConfig().port }, { key: "MINIO_URL", value: getMinioConfig().url }, + { key: "REDIS_URL", value: getRedisConfig().url }, ] for (const config of configs.filter(x => !!x.value)) { From f53faff7ad80fd7530a20036cd51d22d5933048d Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 12:27:49 +0000 Subject: [PATCH 04/12] Add LOG_4XX to environment --- packages/backend-core/src/environment.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index d742ca1cc9..9a07695734 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -82,6 +82,7 @@ const environment = { SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", + LOG_4XX: process.env.LOG_4XX, _set(key: any, value: any) { process.env[key] = value // @ts-ignore From cc7eb64a3bcb6be5429c6f232e39d2161a7a6355 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 14:39:24 +0000 Subject: [PATCH 05/12] Rename LOG_4XX to ENABLE_4XX_HTTP_LOGGING and enable by default --- packages/backend-core/src/environment.ts | 2 +- packages/backend-core/src/middleware/errorHandling.ts | 2 +- packages/backend-core/tests/jestEnv.ts | 1 + packages/server/src/tests/jestEnv.ts | 1 + packages/worker/src/tests/jestEnv.ts | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index df44416609..ed7a161160 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -83,7 +83,7 @@ const environment = { SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", - LOG_4XX: process.env.LOG_4XX, + ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts index 1baaa92501..5ac70c33e5 100644 --- a/packages/backend-core/src/middleware/errorHandling.ts +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -9,7 +9,7 @@ export async function errorHandling(ctx: any, next: any) { const status = err.status || err.statusCode || 500 ctx.status = status - if (status > 499 || env.LOG_4XX) { + if (status > 499 || env.ENABLE_4XX_HTTP_LOGGING) { ctx.log.error(err) } diff --git a/packages/backend-core/tests/jestEnv.ts b/packages/backend-core/tests/jestEnv.ts index 71cf865737..ec8de2942e 100644 --- a/packages/backend-core/tests/jestEnv.ts +++ b/packages/backend-core/tests/jestEnv.ts @@ -3,3 +3,4 @@ process.env.MULTI_TENANCY = "1" process.env.NODE_ENV = "jest" process.env.MOCK_REDIS = "1" process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.ENABLE_4XX_HTTP_LOGGING = "0" diff --git a/packages/server/src/tests/jestEnv.ts b/packages/server/src/tests/jestEnv.ts index b1ef038c1b..c567b260b3 100644 --- a/packages/server/src/tests/jestEnv.ts +++ b/packages/server/src/tests/jestEnv.ts @@ -6,4 +6,5 @@ process.env.MULTI_TENANCY = "1" // @ts-ignore process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.ENABLE_4XX_HTTP_LOGGING = "0" process.env.MOCK_REDIS = "1" diff --git a/packages/worker/src/tests/jestEnv.ts b/packages/worker/src/tests/jestEnv.ts index 602a505c1b..061897451e 100644 --- a/packages/worker/src/tests/jestEnv.ts +++ b/packages/worker/src/tests/jestEnv.ts @@ -2,6 +2,7 @@ process.env.SELF_HOSTED = "0" process.env.NODE_ENV = "jest" process.env.JWT_SECRET = "test-jwtsecret" process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.ENABLE_4XX_HTTP_LOGGING = "0" process.env.MULTI_TENANCY = "1" process.env.MINIO_URL = "http://localhost" process.env.MINIO_ACCESS_KEY = "test" From cacf275a997fa1274a05a98b3bc86637ec69a8dd Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 21 Feb 2023 08:23:53 +0000 Subject: [PATCH 06/12] Prevent SSO users from setting / resetting a password (#9672) * Prevent SSO users from setting / resetting a password * Add support for ENABLE_SSO_MAINTENANCE_MODE * Add typing to self api and build out user update sdk * Integrate sso checks with user sdk. Integrate user sdk with self api * Test fixes * Move self update into SDK * Lock down maintenance mode to admin user * Fix typo * Add health status response and return type signature to accounts.getStatus * Remove some unnecessary comments * Make sso save user function non optional * Remove redundant check on sso auth details provider * Update syncProfilePicture function name to getProfilePictureUrl * Update packages/worker/src/sdk/users/events.ts Co-authored-by: Adria Navarro * Add ENABLE_EMAIL_TEST_MODE flag * Fix for logging in as sso user when existing user has password already * Hide password update and force reset from ui for sso users * Always disable sso maintenance mode in cloud --------- Co-authored-by: Adria Navarro --- .../src/{cloud => accounts}/accounts.ts | 23 +- .../src/{cloud => accounts}/api.ts | 0 packages/backend-core/src/accounts/index.ts | 1 + packages/backend-core/src/auth/auth.ts | 50 +++- .../backend-core/src/auth/tests/auth.spec.ts | 13 + .../backend-core/src/events/identification.ts | 6 +- .../middleware/passport/datasource/google.ts | 11 +- .../src/middleware/passport/local.ts | 56 +--- .../middleware/passport/{ => sso}/google.ts | 28 +- .../src/middleware/passport/{ => sso}/oidc.ts | 40 ++- .../src/middleware/passport/sso/sso.ts | 165 +++++++++++ .../passport/sso/tests/google.spec.ts | 67 +++++ .../passport/sso/tests/oidc.spec.ts | 152 ++++++++++ .../middleware/passport/sso/tests/sso.spec.ts | 196 +++++++++++++ .../middleware/passport/tests/google.spec.js | 79 ----- .../middleware/passport/tests/oidc.spec.js | 144 ---------- .../tests/third-party-common.seq.spec.js | 178 ------------ .../passport/tests/utilities/mock-data.js | 54 ---- .../middleware/passport/third-party-common.ts | 177 ------------ packages/backend-core/src/users.ts | 6 + packages/backend-core/src/utils/utils.ts | 45 +-- .../tests/utilities/mocks/accounts.ts | 13 - .../tests/utilities/mocks/index.ts | 5 +- .../tests/utilities/structures/accounts.ts | 36 ++- .../tests/utilities/structures/index.ts | 4 +- .../tests/utilities/structures/sso.ts | 100 +++++++ .../tests/utilities/structures/users.ts | 70 +++++ .../portal/_components/UserDropdown.svelte | 8 +- .../portal/users/users/[userId].svelte | 9 +- packages/builder/src/stores/portal/auth.js | 1 + packages/server/specs/openapi.json | 12 +- packages/server/specs/openapi.yaml | 6 +- .../src/integrations/tests/couchdb.spec.ts | 2 - packages/types/src/api/account/index.ts | 1 + packages/types/src/api/account/status.ts | 7 + packages/types/src/api/web/auth.ts | 25 ++ packages/types/src/api/web/index.ts | 1 + packages/types/src/api/web/user.ts | 21 +- .../types/src/documents/account/account.ts | 18 +- packages/types/src/documents/global/config.ts | 27 +- packages/types/src/documents/global/user.ts | 63 ++-- packages/types/src/sdk/index.ts | 2 + packages/types/src/sdk/sso.ts | 37 +++ packages/types/src/sdk/user.ts | 12 + packages/worker/scripts/dev/manage.js | 1 + .../worker/src/api/controllers/global/auth.ts | 147 +++++----- .../worker/src/api/controllers/global/self.ts | 70 ++--- .../src/api/controllers/global/users.ts | 43 ++- .../src/api/controllers/system/accounts.ts | 12 +- packages/worker/src/api/routes/global/auth.ts | 17 +- .../src/api/routes/global/tests/auth.spec.ts | 271 +++++++++++++++--- .../src/api/routes/global/tests/self.spec.ts | 7 +- .../api/routes/system/tests/accounts.spec.ts | 8 +- packages/worker/src/environment.ts | 16 +- packages/worker/src/index.ts | 6 + packages/worker/src/sdk/accounts/index.ts | 3 +- .../sdk/accounts/{accounts.ts => metadata.ts} | 1 - packages/worker/src/sdk/auth/auth.ts | 86 ++++++ packages/worker/src/sdk/auth/index.ts | 1 + packages/worker/src/sdk/users/events.ts | 4 + packages/worker/src/sdk/users/index.ts | 1 + .../worker/src/sdk/users/tests/users.spec.ts | 52 ++++ packages/worker/src/sdk/users/users.ts | 97 ++++--- .../worker/src/tests/TestConfiguration.ts | 4 +- packages/worker/src/tests/api/auth.ts | 60 ++-- packages/worker/src/tests/structures/index.ts | 2 - packages/worker/src/tests/structures/users.ts | 37 --- packages/worker/src/utilities/email.ts | 6 +- 68 files changed, 1803 insertions(+), 1120 deletions(-) rename packages/backend-core/src/{cloud => accounts}/accounts.ts (69%) rename packages/backend-core/src/{cloud => accounts}/api.ts (100%) create mode 100644 packages/backend-core/src/accounts/index.ts create mode 100644 packages/backend-core/src/auth/tests/auth.spec.ts rename packages/backend-core/src/middleware/passport/{ => sso}/google.ts (76%) rename packages/backend-core/src/middleware/passport/{ => sso}/oidc.ts (85%) create mode 100644 packages/backend-core/src/middleware/passport/sso/sso.ts create mode 100644 packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts create mode 100644 packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts create mode 100644 packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts delete mode 100644 packages/backend-core/src/middleware/passport/tests/google.spec.js delete mode 100644 packages/backend-core/src/middleware/passport/tests/oidc.spec.js delete mode 100644 packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js delete mode 100644 packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js delete mode 100644 packages/backend-core/src/middleware/passport/third-party-common.ts delete mode 100644 packages/backend-core/tests/utilities/mocks/accounts.ts create mode 100644 packages/backend-core/tests/utilities/structures/sso.ts create mode 100644 packages/backend-core/tests/utilities/structures/users.ts create mode 100644 packages/types/src/api/account/status.ts create mode 100644 packages/types/src/api/web/auth.ts create mode 100644 packages/types/src/sdk/sso.ts create mode 100644 packages/types/src/sdk/user.ts rename packages/worker/src/sdk/accounts/{accounts.ts => metadata.ts} (99%) create mode 100644 packages/worker/src/sdk/auth/auth.ts create mode 100644 packages/worker/src/sdk/auth/index.ts create mode 100644 packages/worker/src/sdk/users/tests/users.spec.ts delete mode 100644 packages/worker/src/tests/structures/users.ts diff --git a/packages/backend-core/src/cloud/accounts.ts b/packages/backend-core/src/accounts/accounts.ts similarity index 69% rename from packages/backend-core/src/cloud/accounts.ts rename to packages/backend-core/src/accounts/accounts.ts index 90fa7ab824..a16d0f1074 100644 --- a/packages/backend-core/src/cloud/accounts.ts +++ b/packages/backend-core/src/accounts/accounts.ts @@ -1,13 +1,24 @@ import API from "./api" import env from "../environment" import { Header } from "../constants" -import { CloudAccount } from "@budibase/types" +import { CloudAccount, HealthStatusResponse } from "@budibase/types" const api = new API(env.ACCOUNT_PORTAL_URL) +/** + * This client is intended to be used in a cloud hosted deploy only. + * Rather than relying on each consumer to perform the necessary environmental checks + * we use the following check to exit early with a undefined response which should be + * handled by the caller. + */ +const EXIT_EARLY = env.SELF_HOSTED || env.DISABLE_ACCOUNT_PORTAL + export const getAccount = async ( email: string ): Promise => { + if (EXIT_EARLY) { + return + } const payload = { email, } @@ -29,6 +40,9 @@ export const getAccount = async ( export const getAccountByTenantId = async ( tenantId: string ): Promise => { + if (EXIT_EARLY) { + return + } const payload = { tenantId, } @@ -47,7 +61,12 @@ export const getAccountByTenantId = async ( return json[0] } -export const getStatus = async () => { +export const getStatus = async (): Promise< + HealthStatusResponse | undefined +> => { + if (EXIT_EARLY) { + return + } const response = await api.get(`/api/status`, { headers: { [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, diff --git a/packages/backend-core/src/cloud/api.ts b/packages/backend-core/src/accounts/api.ts similarity index 100% rename from packages/backend-core/src/cloud/api.ts rename to packages/backend-core/src/accounts/api.ts diff --git a/packages/backend-core/src/accounts/index.ts b/packages/backend-core/src/accounts/index.ts new file mode 100644 index 0000000000..f2ae03040e --- /dev/null +++ b/packages/backend-core/src/accounts/index.ts @@ -0,0 +1 @@ +export * from "./accounts" diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index bbefb2933d..bee245a3ae 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -1,10 +1,11 @@ const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -import { getGlobalDB } from "../tenancy" +import { getGlobalDB } from "../context" const refresh = require("passport-oauth2-refresh") -import { Config } from "../constants" +import { Config, Cookie } from "../constants" import { getScopedConfig } from "../db" +import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { jwt as jwtPassport, local, @@ -15,8 +16,11 @@ import { google, } from "../middleware" import { invalidateUser } from "../cache/user" -import { User } from "@budibase/types" +import { PlatformLogoutOpts, User } from "@budibase/types" import { logAlert } from "../logging" +import * as events from "../events" +import * as userCache from "../cache/user" +import { clearCookie, getCookie } from "../utils" export { auditLog, authError, @@ -29,6 +33,7 @@ export { google, oidc, } from "../middleware" +import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" export const buildAuthMiddleware = authenticated export const buildTenancyMiddleware = tenancy export const buildCsrfMiddleware = csrf @@ -71,7 +76,7 @@ async function refreshOIDCAccessToken( if (!enrichedConfig) { throw new Error("OIDC Config contents invalid") } - strategy = await oidc.strategyFactory(enrichedConfig) + strategy = await oidc.strategyFactory(enrichedConfig, ssoSaveUserNoOp) } catch (err) { console.error(err) throw new Error("Could not refresh OAuth Token") @@ -103,7 +108,11 @@ async function refreshGoogleAccessToken( let strategy try { - strategy = await google.strategyFactory(config, callbackUrl) + strategy = await google.strategyFactory( + config, + callbackUrl, + ssoSaveUserNoOp + ) } catch (err: any) { console.error(err) throw new Error( @@ -161,6 +170,8 @@ export async function refreshOAuthToken( return refreshResponse } +// TODO: Refactor to use user save function instead to prevent the need for +// manually saving and invalidating on callback export async function updateUserOAuth(userId: string, oAuthConfig: any) { const details = { accessToken: oAuthConfig.accessToken, @@ -188,3 +199,32 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) { console.error("Could not update OAuth details for current user", e) } } + +/** + * Logs a user out from budibase. Re-used across account portal and builder. + */ +export async function platformLogout(opts: PlatformLogoutOpts) { + const ctx = opts.ctx + const userId = opts.userId + const keepActiveSession = opts.keepActiveSession + + if (!ctx) throw new Error("Koa context must be supplied to logout.") + + const currentSession = getCookie(ctx, Cookie.Auth) + let sessions = await getSessionsForUser(userId) + + if (keepActiveSession) { + sessions = sessions.filter( + session => session.sessionId !== currentSession.sessionId + ) + } else { + // clear cookies + clearCookie(ctx, Cookie.Auth) + clearCookie(ctx, Cookie.CurrentApp) + } + + const sessionIds = sessions.map(({ sessionId }) => sessionId) + await invalidateSessions(userId, { sessionIds, reason: "logout" }) + await events.auth.logout() + await userCache.invalidateUser(userId) +} diff --git a/packages/backend-core/src/auth/tests/auth.spec.ts b/packages/backend-core/src/auth/tests/auth.spec.ts new file mode 100644 index 0000000000..307f6a63c8 --- /dev/null +++ b/packages/backend-core/src/auth/tests/auth.spec.ts @@ -0,0 +1,13 @@ +import { structures, testEnv } from "../../../tests" +import * as auth from "../auth" +import * as events from "../../events" + +describe("platformLogout", () => { + it("should call platform logout", async () => { + await testEnv.withTenant(async () => { + const ctx = structures.koa.newContext() + await auth.platformLogout({ ctx, userId: "test" }) + expect(events.auth.logout).toBeCalledTimes(1) + }) + }) +}) diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 8ac22b471c..7cade9e14b 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -16,6 +16,7 @@ import { InstallationGroup, UserContext, Group, + isSSOUser, } from "@budibase/types" import { processors } from "./processors" import * as dbUtils from "../db/utils" @@ -166,7 +167,10 @@ const identifyUser = async ( const type = IdentityType.USER let builder = user.builder?.global || false let admin = user.admin?.global || false - let providerType = user.providerType + let providerType + if (isSSOUser(user)) { + providerType = user.providerType + } const accountHolder = account?.budibaseUserId === user._id || false const verified = account && account?.budibaseUserId === user._id ? account.verified : false diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index 65620d7aa3..112f8d2096 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -1,10 +1,11 @@ -import * as google from "../google" +import * as google from "../sso/google" import { Cookie, Config } from "../../../constants" import { clearCookie, getCookie } from "../../../utils" import { getScopedConfig, getPlatformUrl, doWithDB } from "../../../db" import environment from "../../../environment" -import { getGlobalDB } from "../../../tenancy" +import { getGlobalDB } from "../../../context" import { BBContext, Database, SSOProfile } from "@budibase/types" +import { ssoSaveUserNoOp } from "../sso/sso" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy type Passport = { @@ -36,7 +37,11 @@ export async function preAuth( const platformUrl = await getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` - const strategy = await google.strategyFactory(googleConfig, callbackUrl) + const strategy = await google.strategyFactory( + googleConfig, + callbackUrl, + ssoSaveUserNoOp + ) if (!ctx.query.appId || !ctx.query.datasourceId) { ctx.throw(400, "appId and datasourceId query params not present.") diff --git a/packages/backend-core/src/middleware/passport/local.ts b/packages/backend-core/src/middleware/passport/local.ts index 8b85d3734c..e198032532 100644 --- a/packages/backend-core/src/middleware/passport/local.ts +++ b/packages/backend-core/src/middleware/passport/local.ts @@ -1,15 +1,10 @@ import { UserStatus } from "../../constants" -import { compare, newid } from "../../utils" -import env from "../../environment" +import { compare } from "../../utils" import * as users from "../../users" import { authError } from "./utils" -import { createASession } from "../../security/sessions" -import { getTenantId } from "../../tenancy" import { BBContext } from "@budibase/types" -const jwt = require("jsonwebtoken") const INVALID_ERR = "Invalid credentials" -const SSO_NO_PASSWORD = "SSO user does not have a password set" const EXPIRED = "This account has expired. Please reset your password" export const options = { @@ -35,50 +30,25 @@ export async function authenticate( const dbUser = await users.getGlobalUserByEmail(email) if (dbUser == null) { - return authError(done, `User not found: [${email}]`) - } - - // check that the user is currently inactive, if this is the case throw invalid - if (dbUser.status === UserStatus.INACTIVE) { + console.info(`user=${email} could not be found`) return authError(done, INVALID_ERR) } - // check that the user has a stored password before proceeding - if (!dbUser.password) { - if ( - (dbUser.account && dbUser.account.authType === "sso") || // root account sso - dbUser.thirdPartyProfile // internal sso - ) { - return authError(done, SSO_NO_PASSWORD) - } + if (dbUser.status === UserStatus.INACTIVE) { + console.info(`user=${email} is inactive`, dbUser) + return authError(done, INVALID_ERR) + } - console.error("Non SSO usser has no password set", dbUser) + if (!dbUser.password) { + console.info(`user=${email} has no password set`, dbUser) return authError(done, EXPIRED) } - // authenticate - if (await compare(password, dbUser.password)) { - const sessionId = newid() - const tenantId = getTenantId() - - await createASession(dbUser._id!, { sessionId, tenantId }) - - const token = jwt.sign( - { - userId: dbUser._id, - sessionId, - tenantId, - }, - env.JWT_SECRET - ) - // Remove users password in payload - delete dbUser.password - - return done(null, { - ...dbUser, - token, - }) - } else { + if (!(await compare(password, dbUser.password))) { return authError(done, INVALID_ERR) } + + // intentionally remove the users password in payload + delete dbUser.password + return done(null, dbUser) } diff --git a/packages/backend-core/src/middleware/passport/google.ts b/packages/backend-core/src/middleware/passport/sso/google.ts similarity index 76% rename from packages/backend-core/src/middleware/passport/google.ts rename to packages/backend-core/src/middleware/passport/sso/google.ts index dd3dc8b86d..d26d7d6a8d 100644 --- a/packages/backend-core/src/middleware/passport/google.ts +++ b/packages/backend-core/src/middleware/passport/sso/google.ts @@ -1,18 +1,26 @@ -import { ssoCallbackUrl } from "./utils" -import { authenticateThirdParty, SaveUserFunction } from "./third-party-common" -import { ConfigType, GoogleConfig, Database, SSOProfile } from "@budibase/types" +import { ssoCallbackUrl } from "../utils" +import * as sso from "./sso" +import { + ConfigType, + GoogleConfig, + Database, + SSOProfile, + SSOAuthDetails, + SSOProviderType, + SaveSSOUserFunction, +} from "@budibase/types" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy -export function buildVerifyFn(saveUserFn?: SaveUserFunction) { +export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { return ( accessToken: string, refreshToken: string, profile: SSOProfile, done: Function ) => { - const thirdPartyUser = { - provider: profile.provider, // should always be 'google' - providerType: "google", + const details: SSOAuthDetails = { + provider: "google", + providerType: SSOProviderType.GOOGLE, userId: profile.id, profile: profile, email: profile._json.email, @@ -22,8 +30,8 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { }, } - return authenticateThirdParty( - thirdPartyUser, + return sso.authenticate( + details, true, // require local accounts to exist done, saveUserFn @@ -39,7 +47,7 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { export async function strategyFactory( config: GoogleConfig["config"], callbackUrl: string, - saveUserFn?: SaveUserFunction + saveUserFn: SaveSSOUserFunction ) { try { const { clientID, clientSecret } = config diff --git a/packages/backend-core/src/middleware/passport/oidc.ts b/packages/backend-core/src/middleware/passport/sso/oidc.ts similarity index 85% rename from packages/backend-core/src/middleware/passport/oidc.ts rename to packages/backend-core/src/middleware/passport/sso/oidc.ts index 7caa177cf0..1fb44b84a3 100644 --- a/packages/backend-core/src/middleware/passport/oidc.ts +++ b/packages/backend-core/src/middleware/passport/sso/oidc.ts @@ -1,22 +1,20 @@ import fetch from "node-fetch" -import { authenticateThirdParty, SaveUserFunction } from "./third-party-common" -import { ssoCallbackUrl } from "./utils" +import * as sso from "./sso" +import { ssoCallbackUrl } from "../utils" import { ConfigType, - OIDCInnerCfg, + OIDCInnerConfig, Database, SSOProfile, - ThirdPartyUser, - OIDCConfiguration, + OIDCStrategyConfiguration, + SSOAuthDetails, + SSOProviderType, + JwtClaims, + SaveSSOUserFunction, } from "@budibase/types" const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy -type JwtClaims = { - preferred_username: string - email: string -} - -export function buildVerifyFn(saveUserFn?: SaveUserFunction) { +export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { /** * @param {*} issuer The identity provider base URL * @param {*} sub The user ID @@ -39,10 +37,10 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { params: any, done: Function ) => { - const thirdPartyUser: ThirdPartyUser = { + const details: SSOAuthDetails = { // store the issuer info to enable sync in future provider: issuer, - providerType: "oidc", + providerType: SSOProviderType.OIDC, userId: profile.id, profile: profile, email: getEmail(profile, jwtClaims), @@ -52,8 +50,8 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { }, } - return authenticateThirdParty( - thirdPartyUser, + return sso.authenticate( + details, false, // don't require local accounts to exist done, saveUserFn @@ -104,8 +102,8 @@ function validEmail(value: string) { * @returns Dynamically configured Passport OIDC Strategy */ export async function strategyFactory( - config: OIDCConfiguration, - saveUserFn?: SaveUserFunction + config: OIDCStrategyConfiguration, + saveUserFn: SaveSSOUserFunction ) { try { const verify = buildVerifyFn(saveUserFn) @@ -119,14 +117,14 @@ export async function strategyFactory( } export async function fetchStrategyConfig( - enrichedConfig: OIDCInnerCfg, + oidcConfig: OIDCInnerConfig, callbackUrl?: string -): Promise { +): Promise { try { - const { clientID, clientSecret, configUrl } = enrichedConfig + const { clientID, clientSecret, configUrl } = oidcConfig if (!clientID || !clientSecret || !callbackUrl || !configUrl) { - //check for remote config and all required elements + // check for remote config and all required elements throw new Error( "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" ) diff --git a/packages/backend-core/src/middleware/passport/sso/sso.ts b/packages/backend-core/src/middleware/passport/sso/sso.ts new file mode 100644 index 0000000000..2fc1184722 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/sso.ts @@ -0,0 +1,165 @@ +import { generateGlobalUserID } from "../../../db" +import { authError } from "../utils" +import * as users from "../../../users" +import * as context from "../../../context" +import fetch from "node-fetch" +import { + SaveSSOUserFunction, + SaveUserOpts, + SSOAuthDetails, + SSOUser, + User, +} from "@budibase/types" + +// no-op function for user save +// - this allows datasource auth and access token refresh to work correctly +// - prefer no-op over an optional argument to ensure function is provided to login flows +export const ssoSaveUserNoOp: SaveSSOUserFunction = ( + user: SSOUser, + opts: SaveUserOpts +) => Promise.resolve(user) + +/** + * Common authentication logic for third parties. e.g. OAuth, OIDC. + */ +export async function authenticate( + details: SSOAuthDetails, + requireLocalAccount: boolean = true, + done: any, + saveUserFn: SaveSSOUserFunction +) { + if (!saveUserFn) { + throw new Error("Save user function must be provided") + } + if (!details.userId) { + return authError(done, "sso user id required") + } + if (!details.email) { + return authError(done, "sso user email required") + } + + // use the third party id + const userId = generateGlobalUserID(details.userId) + + let dbUser: User | undefined + + // try to load by id + try { + dbUser = await users.getById(userId) + } catch (err: any) { + // abort when not 404 error + if (!err.status || err.status !== 404) { + return authError( + done, + "Unexpected error when retrieving existing user", + err + ) + } + } + + // fallback to loading by email + if (!dbUser) { + dbUser = await users.getGlobalUserByEmail(details.email) + } + + // exit early if there is still no user and auto creation is disabled + if (!dbUser && requireLocalAccount) { + return authError( + done, + "Email does not yet exist. You must set up your local budibase account first." + ) + } + + // first time creation + if (!dbUser) { + // setup a blank user using the third party id + dbUser = { + _id: userId, + email: details.email, + roles: {}, + tenantId: context.getTenantId(), + } + } + + let ssoUser = await syncUser(dbUser, details) + // never prompt for password reset + ssoUser.forceResetPassword = false + + try { + // don't try to re-save any existing password + delete ssoUser.password + // create or sync the user + ssoUser = (await saveUserFn(ssoUser, { + hashPassword: false, + requirePassword: false, + })) as SSOUser + } catch (err: any) { + return authError(done, "Error saving user", err) + } + + return done(null, ssoUser) +} + +async function getProfilePictureUrl(user: User, details: SSOAuthDetails) { + const pictureUrl = details.profile?._json.picture + if (pictureUrl) { + const response = await fetch(pictureUrl) + if (response.status === 200) { + const type = response.headers.get("content-type") as string + if (type.startsWith("image/")) { + return pictureUrl + } + } + } +} + +/** + * @returns a user that has been sync'd with third party information + */ +async function syncUser(user: User, details: SSOAuthDetails): Promise { + let firstName + let lastName + let pictureUrl + let oauth2 + let thirdPartyProfile + + if (details.profile) { + const profile = details.profile + + if (profile.name) { + const name = profile.name + // first name + if (name.givenName) { + firstName = name.givenName + } + // last name + if (name.familyName) { + lastName = name.familyName + } + } + + pictureUrl = await getProfilePictureUrl(user, details) + + thirdPartyProfile = { + ...profile._json, + } + } + + // oauth tokens for future use + if (details.oauth2) { + oauth2 = { + ...details.oauth2, + } + } + + return { + ...user, + provider: details.provider, + providerType: details.providerType, + firstName, + lastName, + thirdPartyProfile, + pictureUrl, + oauth2, + } +} diff --git a/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts new file mode 100644 index 0000000000..eb8ffc9b71 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts @@ -0,0 +1,67 @@ +import { generator, structures } from "../../../../../tests" +import { SSOProviderType } from "@budibase/types" + +jest.mock("passport-google-oauth") +const mockStrategy = require("passport-google-oauth").OAuth2Strategy + +jest.mock("../sso") +import * as _sso from "../sso" +const sso = jest.mocked(_sso) + +const mockSaveUserFn = jest.fn() +const mockDone = jest.fn() + +import * as google from "../google" + +describe("google", () => { + describe("strategyFactory", () => { + const googleConfig = structures.sso.googleConfig() + const callbackUrl = generator.url() + + it("should create successfully create a google strategy", async () => { + await google.strategyFactory(googleConfig, callbackUrl) + + const expectedOptions = { + clientID: googleConfig.clientID, + clientSecret: googleConfig.clientSecret, + callbackURL: callbackUrl, + } + + expect(mockStrategy).toHaveBeenCalledWith( + expectedOptions, + expect.anything() + ) + }) + }) + + describe("authenticate", () => { + const details = structures.sso.authDetails() + details.provider = "google" + details.providerType = SSOProviderType.GOOGLE + + const profile = details.profile! + profile.provider = "google" + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("delegates authentication to third party common", async () => { + const authenticate = await google.buildVerifyFn(mockSaveUserFn) + + await authenticate( + details.oauth2.accessToken, + details.oauth2.refreshToken!, + profile, + mockDone + ) + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + true, + mockDone, + mockSaveUserFn + ) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts new file mode 100644 index 0000000000..a705739bd6 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts @@ -0,0 +1,152 @@ +import { generator, mocks, structures } from "../../../../../tests" +import { + JwtClaims, + OIDCInnerConfig, + SSOAuthDetails, + SSOProviderType, +} from "@budibase/types" +import * as _sso from "../sso" +import * as oidc from "../oidc" + +jest.mock("@techpass/passport-openidconnect") +const mockStrategy = require("@techpass/passport-openidconnect").Strategy + +jest.mock("../sso") +const sso = jest.mocked(_sso) + +const mockSaveUser = jest.fn() +const mockDone = jest.fn() + +describe("oidc", () => { + const callbackUrl = generator.url() + const oidcConfig: OIDCInnerConfig = structures.sso.oidcConfig() + const wellKnownConfig = structures.sso.oidcWellKnownConfig() + + function mockRetrieveWellKnownConfig() { + // mock the request to retrieve the oidc configuration + mocks.fetch.mockReturnValue({ + ok: true, + json: () => wellKnownConfig, + }) + } + + beforeEach(() => { + mockRetrieveWellKnownConfig() + }) + + describe("strategyFactory", () => { + it("should create successfully create an oidc strategy", async () => { + const strategyConfiguration = await oidc.fetchStrategyConfig( + oidcConfig, + callbackUrl + ) + await oidc.strategyFactory(strategyConfiguration, mockSaveUser) + + expect(mocks.fetch).toHaveBeenCalledWith(oidcConfig.configUrl) + + const expectedOptions = { + issuer: wellKnownConfig.issuer, + authorizationURL: wellKnownConfig.authorization_endpoint, + tokenURL: wellKnownConfig.token_endpoint, + userInfoURL: wellKnownConfig.userinfo_endpoint, + clientID: oidcConfig.clientID, + clientSecret: oidcConfig.clientSecret, + callbackURL: callbackUrl, + } + expect(mockStrategy).toHaveBeenCalledWith( + expectedOptions, + expect.anything() + ) + }) + }) + + describe("authenticate", () => { + const details: SSOAuthDetails = structures.sso.authDetails() + details.providerType = SSOProviderType.OIDC + const profile = details.profile! + const issuer = profile.provider + + const sub = generator.string() + const idToken = generator.string() + const params = {} + + let authenticateFn: any + let jwtClaims: JwtClaims + + beforeEach(async () => { + jest.clearAllMocks() + authenticateFn = await oidc.buildVerifyFn(mockSaveUser) + }) + + async function authenticate() { + await authenticateFn( + issuer, + sub, + profile, + jwtClaims, + details.oauth2.accessToken, + details.oauth2.refreshToken, + idToken, + params, + mockDone + ) + } + + it("passes auth details to sso module", async () => { + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT email to get email", async () => { + delete profile._json.email + + jwtClaims = { + email: details.email, + } + + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT username to get email", async () => { + delete profile._json.email + + jwtClaims = { + email: details.email, + } + + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT invalid username to get email", async () => { + delete profile._json.email + + jwtClaims = { + preferred_username: "invalidUsername", + } + + await expect(authenticate()).rejects.toThrow( + "Could not determine user email from profile" + ) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts new file mode 100644 index 0000000000..ae42fc01ea --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts @@ -0,0 +1,196 @@ +import { structures, testEnv, mocks } from "../../../../../tests" +import { SSOAuthDetails, User } from "@budibase/types" + +import { HTTPError } from "../../../../errors" +import * as sso from "../sso" +import * as context from "../../../../context" + +const mockDone = jest.fn() +const mockSaveUser = jest.fn() + +jest.mock("../../../../users") +import * as _users from "../../../../users" +const users = jest.mocked(_users) + +const getErrorMessage = () => { + return mockDone.mock.calls[0][2].message +} + +describe("sso", () => { + describe("authenticate", () => { + beforeEach(() => { + jest.clearAllMocks() + testEnv.singleTenant() + }) + + describe("validation", () => { + const testValidation = async ( + details: SSOAuthDetails, + message: string + ) => { + await sso.authenticate(details, false, mockDone, mockSaveUser) + + expect(mockDone.mock.calls.length).toBe(1) + expect(getErrorMessage()).toContain(message) + } + + it("user id fails", async () => { + const details = structures.sso.authDetails() + details.userId = undefined! + + await testValidation(details, "sso user id required") + }) + + it("email fails", async () => { + const details = structures.sso.authDetails() + details.email = undefined! + + await testValidation(details, "sso user email required") + }) + }) + + function mockGetProfilePicture() { + mocks.fetch.mockReturnValueOnce( + Promise.resolve({ + status: 200, + headers: { get: () => "image/" }, + }) + ) + } + + describe("when the user doesn't exist", () => { + let user: User + let details: SSOAuthDetails + + beforeEach(() => { + users.getById.mockImplementationOnce(() => { + throw new HTTPError("", 404) + }) + mockGetProfilePicture() + + user = structures.users.user() + delete user._rev + delete user._id + + details = structures.sso.authDetails(user) + details.userId = structures.uuid() + }) + + describe("when a local account is required", () => { + it("returns an error message", async () => { + const details = structures.sso.authDetails() + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + expect(mockDone.mock.calls.length).toBe(1) + expect(getErrorMessage()).toContain( + "Email does not yet exist. You must set up your local budibase account first." + ) + }) + }) + + describe("when a local account isn't required", () => { + it("creates and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ user, details }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, false, mockDone, mockSaveUser) + + // default roles for new user + ssoUser.roles = {} + + // modified external id to match user format + ssoUser._id = "us_" + details.userId + + // new sso user won't have a password + delete ssoUser.password + + // new user isn't saved with rev + delete ssoUser._rev + + // tenant id added + ssoUser.tenantId = context.getTenantId() + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + }) + + describe("when the user exists", () => { + let existingUser: User + let details: SSOAuthDetails + + beforeEach(() => { + existingUser = structures.users.user() + existingUser._id = structures.uuid() + details = structures.sso.authDetails(existingUser) + mockGetProfilePicture() + }) + + describe("exists by email", () => { + beforeEach(() => { + users.getById.mockImplementationOnce(() => { + throw new HTTPError("", 404) + }) + users.getGlobalUserByEmail.mockReturnValueOnce( + Promise.resolve(existingUser) + ) + }) + + it("syncs and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ + user: existingUser, + details, + }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + // roles preserved + ssoUser.roles = existingUser.roles + + // existing id preserved + ssoUser._id = existingUser._id + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + + describe("exists by id", () => { + beforeEach(() => { + users.getById.mockReturnValueOnce(Promise.resolve(existingUser)) + }) + + it("syncs and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ + user: existingUser, + details, + }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + // roles preserved + ssoUser.roles = existingUser.roles + + // existing id preserved + ssoUser._id = existingUser._id + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/tests/google.spec.js b/packages/backend-core/src/middleware/passport/tests/google.spec.js deleted file mode 100644 index c5580ea309..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/google.spec.js +++ /dev/null @@ -1,79 +0,0 @@ -// Mock data - -const { data } = require("./utilities/mock-data") - -const TENANT_ID = "default" - -const googleConfig = { - clientID: data.clientID, - clientSecret: data.clientSecret, -} - -const profile = { - id: "mockId", - _json: { - email : data.email - }, - provider: "google" -} - -const user = data.buildThirdPartyUser("google", "google", profile) - -describe("google", () => { - describe("strategyFactory", () => { - // mock passport strategy factory - jest.mock("passport-google-oauth") - const mockStrategy = require("passport-google-oauth").OAuth2Strategy - - it("should create successfully create a google strategy", async () => { - const google = require("../google") - - const callbackUrl = `/api/global/auth/${TENANT_ID}/google/callback` - await google.strategyFactory(googleConfig, callbackUrl) - - const expectedOptions = { - clientID: googleConfig.clientID, - clientSecret: googleConfig.clientSecret, - callbackURL: callbackUrl, - } - - expect(mockStrategy).toHaveBeenCalledWith( - expectedOptions, - expect.anything() - ) - }) - }) - - describe("authenticate", () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - // mock third party common authentication - jest.mock("../third-party-common") - const authenticateThirdParty = require("../third-party-common").authenticateThirdParty - - // mock the passport callback - const mockDone = jest.fn() - - it("delegates authentication to third party common", async () => { - const google = require("../google") - const mockSaveUserFn = jest.fn() - const authenticate = await google.buildVerifyFn(mockSaveUserFn) - - await authenticate( - data.accessToken, - data.refreshToken, - profile, - mockDone - ) - - expect(authenticateThirdParty).toHaveBeenCalledWith( - user, - true, - mockDone, - mockSaveUserFn) - }) - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js deleted file mode 100644 index 4c8aa94ddf..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ /dev/null @@ -1,144 +0,0 @@ -// Mock data -const mockFetch = require("node-fetch") -const { data } = require("./utilities/mock-data") -const issuer = "mockIssuer" -const sub = "mockSub" -const profile = { - id: "mockId", - _json: { - email : data.email - } -} -let jwtClaims = {} -const idToken = "mockIdToken" -const params = {} - -const callbackUrl = "http://somecallbackurl" - -// response from .well-known/openid-configuration -const oidcConfigUrlResponse = { - issuer: issuer, - authorization_endpoint: "mockAuthorizationEndpoint", - token_endpoint: "mockTokenEndpoint", - userinfo_endpoint: "mockUserInfoEndpoint" -} - -const oidcConfig = { - configUrl: "http://someconfigurl", - clientID: data.clientID, - clientSecret: data.clientSecret, -} - -const user = data.buildThirdPartyUser(issuer, "oidc", profile) - -describe("oidc", () => { - describe("strategyFactory", () => { - // mock passport strategy factory - jest.mock("@techpass/passport-openidconnect") - const mockStrategy = require("@techpass/passport-openidconnect").Strategy - - // mock the request to retrieve the oidc configuration - mockFetch.mockReturnValue({ - ok: true, - json: () => oidcConfigUrlResponse - }) - - it("should create successfully create an oidc strategy", async () => { - const oidc = require("../oidc") - const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl) - await oidc.strategyFactory(enrichedConfig, callbackUrl) - - expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) - - const expectedOptions = { - issuer: oidcConfigUrlResponse.issuer, - authorizationURL: oidcConfigUrlResponse.authorization_endpoint, - tokenURL: oidcConfigUrlResponse.token_endpoint, - userInfoURL: oidcConfigUrlResponse.userinfo_endpoint, - clientID: oidcConfig.clientID, - clientSecret: oidcConfig.clientSecret, - callbackURL: callbackUrl, - } - expect(mockStrategy).toHaveBeenCalledWith( - expectedOptions, - expect.anything() - ) - }) - }) - - describe("authenticate", () => { - afterEach(() => { - jest.clearAllMocks() - }); - - // mock third party common authentication - jest.mock("../third-party-common") - const authenticateThirdParty = require("../third-party-common").authenticateThirdParty - - // mock the passport callback - const mockDone = jest.fn() - const mockSaveUserFn = jest.fn() - - async function doAuthenticate() { - const oidc = require("../oidc") - const authenticate = await oidc.buildVerifyFn(mockSaveUserFn) - - await authenticate( - issuer, - sub, - profile, - jwtClaims, - data.accessToken, - data.refreshToken, - idToken, - params, - mockDone - ) - } - - async function doTest() { - await doAuthenticate() - - expect(authenticateThirdParty).toHaveBeenCalledWith( - user, - false, - mockDone, - mockSaveUserFn, - ) - } - - it("delegates authentication to third party common", async () => { - await doTest() - }) - - it("uses JWT email to get email", async () => { - delete profile._json.email - jwtClaims = { - email : "mock@budibase.com" - } - - await doTest() - }) - - it("uses JWT username to get email", async () => { - delete profile._json.email - jwtClaims = { - preferred_username : "mock@budibase.com" - } - - await doTest() - }) - - it("uses JWT invalid username to get email", async () => { - delete profile._json.email - - jwtClaims = { - preferred_username : "invalidUsername" - } - - await expect(doAuthenticate()).rejects.toThrow("Could not determine user email from profile"); - }) - - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js b/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js deleted file mode 100644 index d377d602f1..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js +++ /dev/null @@ -1,178 +0,0 @@ -require("../../../../tests") -const { authenticateThirdParty } = require("../third-party-common") -const { data } = require("./utilities/mock-data") -const { DEFAULT_TENANT_ID } = require("../../../constants") - -const { generateGlobalUserID } = require("../../../db/utils") -const { newid } = require("../../../utils") -const { doWithGlobalDB, doInTenant } = require("../../../tenancy") - -const done = jest.fn() - -const getErrorMessage = () => { - return done.mock.calls[0][2].message -} - -const saveUser = async (user) => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - return await db.put(user) - }) -} - -function authenticate(user, requireLocal, saveFn) { - return doInTenant(DEFAULT_TENANT_ID, () => { - return authenticateThirdParty(user, requireLocal, done, saveFn) - }) -} - -describe("third party common", () => { - describe("authenticateThirdParty", () => { - let thirdPartyUser - - beforeEach(() => { - thirdPartyUser = data.buildThirdPartyUser() - }) - - afterEach(async () => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - jest.clearAllMocks() - await db.destroy() - }) - }) - - describe("validation", () => { - const testValidation = async (message) => { - await authenticate(thirdPartyUser, false, saveUser) - expect(done.mock.calls.length).toBe(1) - expect(getErrorMessage()).toContain(message) - } - - it("provider fails", async () => { - delete thirdPartyUser.provider - await testValidation("third party user provider required") - }) - - it("user id fails", async () => { - delete thirdPartyUser.userId - await testValidation("third party user id required") - }) - - it("email fails", async () => { - delete thirdPartyUser.email - await testValidation("third party user email required") - }) - }) - - const expectUserIsAuthenticated = () => { - const user = done.mock.calls[0][1] - expect(user).toBeDefined() - expect(user._id).toBeDefined() - expect(user._rev).toBeDefined() - expect(user.token).toBeDefined() - return user - } - - const expectUserIsSynced = (user, thirdPartyUser) => { - expect(user.provider).toBe(thirdPartyUser.provider) - expect(user.firstName).toBe(thirdPartyUser.profile.name.givenName) - expect(user.lastName).toBe(thirdPartyUser.profile.name.familyName) - expect(user.thirdPartyProfile).toStrictEqual(thirdPartyUser.profile._json) - expect(user.oauth2).toStrictEqual(thirdPartyUser.oauth2) - } - - describe("when the user doesn't exist", () => { - describe("when a local account is required", () => { - it("returns an error message", async () => { - await authenticate(thirdPartyUser, true, saveUser) - expect(done.mock.calls.length).toBe(1) - expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.") - }) - }) - - describe("when a local account isn't required", () => { - it("creates and authenticates the user", async () => { - await authenticate(thirdPartyUser, false, saveUser) - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expect(user.roles).toStrictEqual({}) - }) - }) - }) - - describe("when the user exists", () => { - let dbUser - let id - let email - - const createUser = async () => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - dbUser = { - _id: id, - email: email, - } - const response = await db.put(dbUser) - dbUser._rev = response.rev - return dbUser - }) - } - - const expectUserIsUpdated = (user) => { - // id is unchanged - expect(user._id).toBe(id) - // user is updated - expect(user._rev).not.toBe(dbUser._rev) - } - - describe("exists by email", () => { - beforeEach(async () => { - id = generateGlobalUserID(newid()) // random id - email = thirdPartyUser.email // matching email - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - }) - }) - - describe("exists by email with different casing", () => { - beforeEach(async () => { - id = generateGlobalUserID(newid()) // random id - email = thirdPartyUser.email.toUpperCase() // matching email except for casing - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - expect(user.email).toBe(thirdPartyUser.email.toUpperCase()) - }) - }) - - - describe("exists by id", () => { - beforeEach(async () => { - id = generateGlobalUserID(thirdPartyUser.userId) // matching id - email = "test@test.com" // random email - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - }) - }) - }) - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js b/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js deleted file mode 100644 index 00ae82e47e..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js +++ /dev/null @@ -1,54 +0,0 @@ -// Mock Data - -const mockClientID = "mockClientID" -const mockClientSecret = "mockClientSecret" - -const mockEmail = "mock@budibase.com" -const mockAccessToken = "mockAccessToken" -const mockRefreshToken = "mockRefreshToken" - -const mockProvider = "mockProvider" -const mockProviderType = "mockProviderType" - -const mockProfile = { - id: "mockId", - name: { - givenName: "mockGivenName", - familyName: "mockFamilyName", - }, - _json: { - email: mockEmail, - }, -} - -const buildOauth2 = ( - accessToken = mockAccessToken, - refreshToken = mockRefreshToken -) => ({ - accessToken: accessToken, - refreshToken: refreshToken, -}) - -const buildThirdPartyUser = ( - provider = mockProvider, - providerType = mockProviderType, - profile = mockProfile, - email = mockEmail, - oauth2 = buildOauth2() -) => ({ - provider: provider, - providerType: providerType, - userId: profile.id, - profile: profile, - email: email, - oauth2: oauth2, -}) - -exports.data = { - clientID: mockClientID, - clientSecret: mockClientSecret, - email: mockEmail, - accessToken: mockAccessToken, - refreshToken: mockRefreshToken, - buildThirdPartyUser, -} diff --git a/packages/backend-core/src/middleware/passport/third-party-common.ts b/packages/backend-core/src/middleware/passport/third-party-common.ts deleted file mode 100644 index 9d7b93f370..0000000000 --- a/packages/backend-core/src/middleware/passport/third-party-common.ts +++ /dev/null @@ -1,177 +0,0 @@ -import env from "../../environment" -import { generateGlobalUserID } from "../../db" -import { authError } from "./utils" -import { newid } from "../../utils" -import { createASession } from "../../security/sessions" -import * as users from "../../users" -import { getGlobalDB, getTenantId } from "../../tenancy" -import fetch from "node-fetch" -import { ThirdPartyUser } from "@budibase/types" -const jwt = require("jsonwebtoken") - -type SaveUserOpts = { - requirePassword?: boolean - hashPassword?: boolean - currentUserId?: string -} - -export type SaveUserFunction = ( - user: ThirdPartyUser, - opts: SaveUserOpts -) => Promise - -/** - * Common authentication logic for third parties. e.g. OAuth, OIDC. - */ -export async function authenticateThirdParty( - thirdPartyUser: ThirdPartyUser, - requireLocalAccount: boolean = true, - done: Function, - saveUserFn?: SaveUserFunction -) { - if (!saveUserFn) { - throw new Error("Save user function must be provided") - } - if (!thirdPartyUser.provider) { - return authError(done, "third party user provider required") - } - if (!thirdPartyUser.userId) { - return authError(done, "third party user id required") - } - if (!thirdPartyUser.email) { - return authError(done, "third party user email required") - } - - // use the third party id - const userId = generateGlobalUserID(thirdPartyUser.userId) - const db = getGlobalDB() - - let dbUser - - // try to load by id - try { - dbUser = await db.get(userId) - } catch (err: any) { - // abort when not 404 error - if (!err.status || err.status !== 404) { - return authError( - done, - "Unexpected error when retrieving existing user", - err - ) - } - } - - // fallback to loading by email - if (!dbUser) { - dbUser = await users.getGlobalUserByEmail(thirdPartyUser.email) - } - - // exit early if there is still no user and auto creation is disabled - if (!dbUser && requireLocalAccount) { - return authError( - done, - "Email does not yet exist. You must set up your local budibase account first." - ) - } - - // first time creation - if (!dbUser) { - // setup a blank user using the third party id - dbUser = { - _id: userId, - email: thirdPartyUser.email, - roles: {}, - } - } - - dbUser = await syncUser(dbUser, thirdPartyUser) - - // never prompt for password reset - dbUser.forceResetPassword = false - - // create or sync the user - try { - await saveUserFn(dbUser, { hashPassword: false, requirePassword: false }) - } catch (err: any) { - return authError(done, "Error saving user", err) - } - - // now that we're sure user exists, load them from the db - dbUser = await db.get(dbUser._id) - - // authenticate - const sessionId = newid() - const tenantId = getTenantId() - await createASession(dbUser._id, { sessionId, tenantId }) - - dbUser.token = jwt.sign( - { - userId: dbUser._id, - sessionId, - }, - env.JWT_SECRET - ) - - return done(null, dbUser) -} - -async function syncProfilePicture( - user: ThirdPartyUser, - thirdPartyUser: ThirdPartyUser -) { - const pictureUrl = thirdPartyUser.profile?._json.picture - if (pictureUrl) { - const response = await fetch(pictureUrl) - - if (response.status === 200) { - const type = response.headers.get("content-type") as string - if (type.startsWith("image/")) { - user.pictureUrl = pictureUrl - } - } - } - - return user -} - -/** - * @returns a user that has been sync'd with third party information - */ -async function syncUser(user: ThirdPartyUser, thirdPartyUser: ThirdPartyUser) { - // provider - user.provider = thirdPartyUser.provider - user.providerType = thirdPartyUser.providerType - - if (thirdPartyUser.profile) { - const profile = thirdPartyUser.profile - - if (profile.name) { - const name = profile.name - // first name - if (name.givenName) { - user.firstName = name.givenName - } - // last name - if (name.familyName) { - user.lastName = name.familyName - } - } - - user = await syncProfilePicture(user, thirdPartyUser) - - // profile - user.thirdPartyProfile = { - ...profile._json, - } - } - - // oauth tokens for future use - if (thirdPartyUser.oauth2) { - user.oauth2 = { - ...thirdPartyUser.oauth2, - } - } - - return user -} diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 1720a79a83..ef76af390d 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -8,6 +8,7 @@ import { } from "./db" import { BulkDocsResponse, User } from "@budibase/types" import { getGlobalDB } from "./context" +import * as context from "./context" export const bulkGetGlobalUsersById = async (userIds: string[]) => { const db = getGlobalDB() @@ -24,6 +25,11 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => { return (await db.bulkDocs(users)) as BulkDocsResponse } +export async function getById(id: string): Promise { + const db = context.getGlobalDB() + return db.get(id) +} + /** * Given an email address this will use a view to search through * all the users to find one with this email address. diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index c608686431..3731e134ad 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -2,23 +2,15 @@ import { getAllApps, queryGlobalView } from "../db" import { options } from "../middleware/passport/jwt" import { Header, - Cookie, MAX_VALID_DATE, DocumentType, SEPARATOR, ViewName, } from "../constants" import env from "../environment" -import * as userCache from "../cache/user" -import { getSessionsForUser, invalidateSessions } from "../security/sessions" -import * as events from "../events" import * as tenancy from "../tenancy" -import { - App, - Ctx, - PlatformLogoutOpts, - TenantResolutionStrategy, -} from "@budibase/types" +import * as context from "../context" +import { App, Ctx, TenantResolutionStrategy } from "@budibase/types" import { SetOption } from "cookies" const jwt = require("jsonwebtoken") @@ -38,7 +30,7 @@ export async function resolveAppUrl(ctx: Ctx) { const appUrl = ctx.path.split("/")[2] let possibleAppUrl = `/${appUrl.toLowerCase()}` - let tenantId: string | null = tenancy.getTenantId() + let tenantId: string | null = context.getTenantId() if (env.MULTI_TENANCY) { // always use the tenant id from the subdomain in multi tenancy // this ensures the logged-in user tenant id doesn't overwrite @@ -49,7 +41,7 @@ export async function resolveAppUrl(ctx: Ctx) { } // search prod apps for a url that matches - const apps: App[] = await tenancy.doInTenant(tenantId, () => + const apps: App[] = await context.doInTenant(tenantId, () => getAllApps({ dev: false }) ) const app = apps.filter( @@ -222,35 +214,6 @@ export async function getBuildersCount() { return builders.length } -/** - * Logs a user out from budibase. Re-used across account portal and builder. - */ -export async function platformLogout(opts: PlatformLogoutOpts) { - const ctx = opts.ctx - const userId = opts.userId - const keepActiveSession = opts.keepActiveSession - - if (!ctx) throw new Error("Koa context must be supplied to logout.") - - const currentSession = getCookie(ctx, Cookie.Auth) - let sessions = await getSessionsForUser(userId) - - if (keepActiveSession) { - sessions = sessions.filter( - session => session.sessionId !== currentSession.sessionId - ) - } else { - // clear cookies - clearCookie(ctx, Cookie.Auth) - clearCookie(ctx, Cookie.CurrentApp) - } - - const sessionIds = sessions.map(({ sessionId }) => sessionId) - await invalidateSessions(userId, { sessionIds, reason: "logout" }) - await events.auth.logout() - await userCache.invalidateUser(userId) -} - export function timeout(timeMs: number) { return new Promise(resolve => setTimeout(resolve, timeMs)) } diff --git a/packages/backend-core/tests/utilities/mocks/accounts.ts b/packages/backend-core/tests/utilities/mocks/accounts.ts deleted file mode 100644 index e40d32b276..0000000000 --- a/packages/backend-core/tests/utilities/mocks/accounts.ts +++ /dev/null @@ -1,13 +0,0 @@ -const mockGetAccount = jest.fn() -const mockGetAccountByTenantId = jest.fn() -const mockGetStatus = jest.fn() - -jest.mock("../../../src/cloud/accounts", () => ({ - getAccount: mockGetAccount, - getAccountByTenantId: mockGetAccountByTenantId, - getStatus: mockGetStatus, -})) - -export const getAccount = mockGetAccount -export const getAccountByTenantId = mockGetAccountByTenantId -export const getStatus = mockGetStatus diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts index 401fd7d7a7..f5f45c0342 100644 --- a/packages/backend-core/tests/utilities/mocks/index.ts +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -1,4 +1,7 @@ -export * as accounts from "./accounts" +jest.mock("../../../src/accounts") +import * as _accounts from "../../../src/accounts" +export const accounts = jest.mocked(_accounts) + export * as date from "./date" export * as licenses from "./licenses" export { default as fetch } from "./fetch" diff --git a/packages/backend-core/tests/utilities/structures/accounts.ts b/packages/backend-core/tests/utilities/structures/accounts.ts index f1718aecc0..6bfeedf196 100644 --- a/packages/backend-core/tests/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/utilities/structures/accounts.ts @@ -1,6 +1,15 @@ import { generator, uuid } from "." import * as db from "../../../src/db/utils" -import { Account, AuthType, CloudAccount, Hosting } from "@budibase/types" +import { + Account, + AccountSSOProvider, + AccountSSOProviderType, + AuthType, + CloudAccount, + Hosting, + SSOAccount, +} from "@budibase/types" +import _ from "lodash" export const account = (): Account => { return { @@ -27,3 +36,28 @@ export const cloudAccount = (): CloudAccount => { budibaseUserId: db.generateGlobalUserID(), } } + +function providerType(): AccountSSOProviderType { + return _.sample( + Object.values(AccountSSOProviderType) + ) as AccountSSOProviderType +} + +function provider(): AccountSSOProvider { + return _.sample(Object.values(AccountSSOProvider)) as AccountSSOProvider +} + +export function ssoAccount(): SSOAccount { + return { + ...cloudAccount(), + authType: AuthType.SSO, + oauth2: { + accessToken: generator.string(), + refreshToken: generator.string(), + }, + pictureUrl: generator.url(), + provider: provider(), + providerType: providerType(), + thirdPartyProfile: {}, + } +} diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts index e74751e479..d0073ba851 100644 --- a/packages/backend-core/tests/utilities/structures/index.ts +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -5,8 +5,10 @@ export const generator = new Chance() export * as accounts from "./accounts" export * as apps from "./apps" +export * as db from "./db" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" +export * as sso from "./sso" export * as tenant from "./tenants" -export * as db from "./db" +export * as users from "./users" diff --git a/packages/backend-core/tests/utilities/structures/sso.ts b/packages/backend-core/tests/utilities/structures/sso.ts new file mode 100644 index 0000000000..ad5e8e87ef --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/sso.ts @@ -0,0 +1,100 @@ +import { + GoogleInnerConfig, + JwtClaims, + OIDCInnerConfig, + OIDCWellKnownConfig, + SSOAuthDetails, + SSOProfile, + SSOProviderType, + User, +} from "@budibase/types" +import { uuid, generator, users, email } from "./index" +import _ from "lodash" + +export function providerType(): SSOProviderType { + return _.sample(Object.values(SSOProviderType)) as SSOProviderType +} + +export function ssoProfile(user?: User): SSOProfile { + if (!user) { + user = users.user() + } + return { + id: user._id!, + name: { + givenName: user.firstName, + familyName: user.lastName, + }, + _json: { + email: user.email, + picture: "http://test.com", + }, + provider: generator.string(), + } +} + +export function authDetails(user?: User): SSOAuthDetails { + if (!user) { + user = users.user() + } + + const userId = user._id || uuid() + const provider = generator.string() + + const profile = ssoProfile(user) + profile.provider = provider + profile.id = userId + + return { + email: user.email, + oauth2: { + refreshToken: generator.string(), + accessToken: generator.string(), + }, + profile, + provider, + providerType: providerType(), + userId, + } +} + +// OIDC + +export function oidcConfig(): OIDCInnerConfig { + return { + uuid: uuid(), + activated: true, + logo: "", + name: generator.string(), + configUrl: "http://someconfigurl", + clientID: generator.string(), + clientSecret: generator.string(), + } +} + +// response from .well-known/openid-configuration +export function oidcWellKnownConfig(): OIDCWellKnownConfig { + return { + issuer: generator.string(), + authorization_endpoint: generator.url(), + token_endpoint: generator.url(), + userinfo_endpoint: generator.url(), + } +} + +export function jwtClaims(): JwtClaims { + return { + email: email(), + preferred_username: email(), + } +} + +// GOOGLE + +export function googleConfig(): GoogleInnerConfig { + return { + activated: true, + clientID: generator.string(), + clientSecret: generator.string(), + } +} diff --git a/packages/backend-core/tests/utilities/structures/users.ts b/packages/backend-core/tests/utilities/structures/users.ts new file mode 100644 index 0000000000..332c27ca12 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/users.ts @@ -0,0 +1,70 @@ +import { generator } from "../" +import { + AdminUser, + BuilderUser, + SSOAuthDetails, + SSOUser, + User, +} from "@budibase/types" +import { v4 as uuid } from "uuid" +import * as sso from "./sso" + +export const newEmail = () => { + return `${uuid()}@test.com` +} + +export const user = (userProps?: any): User => { + return { + email: newEmail(), + password: "test", + roles: { app_test: "admin" }, + firstName: generator.first(), + lastName: generator.last(), + pictureUrl: "http://test.com", + ...userProps, + } +} + +export const adminUser = (userProps?: any): AdminUser => { + return { + ...user(userProps), + admin: { + global: true, + }, + builder: { + global: true, + }, + } +} + +export const builderUser = (userProps?: any): BuilderUser => { + return { + ...user(userProps), + builder: { + global: true, + }, + } +} + +export function ssoUser( + opts: { user?: any; details?: SSOAuthDetails } = {} +): SSOUser { + const base = user(opts.user) + delete base.password + + if (!opts.details) { + opts.details = sso.authDetails(base) + } + + return { + ...base, + forceResetPassword: false, + oauth2: opts.details?.oauth2, + provider: opts.details?.provider!, + providerType: opts.details?.providerType!, + thirdPartyProfile: { + email: base.email, + picture: base.pictureUrl, + }, + } +} diff --git a/packages/builder/src/pages/builder/portal/_components/UserDropdown.svelte b/packages/builder/src/pages/builder/portal/_components/UserDropdown.svelte index d396edd4e2..935d69812f 100644 --- a/packages/builder/src/pages/builder/portal/_components/UserDropdown.svelte +++ b/packages/builder/src/pages/builder/portal/_components/UserDropdown.svelte @@ -30,9 +30,11 @@ My profile themeModal.show()}>Theme - updatePasswordModal.show()}> - Update password - + {#if !$auth.isSSO} + updatePasswordModal.show()}> + Update password + + {/if} apiKeyModal.show()}> View API key diff --git a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte index b14e3420cc..1c26273a8d 100644 --- a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte @@ -81,6 +81,7 @@ let user let loaded = false + $: isSSO = !!user?.provider $: readonly = !$auth.isAdmin $: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : "" $: privileged = user?.admin?.global || user?.builder?.global @@ -246,9 +247,11 @@ - - Force password reset - + {#if !isSSO} + + Force password reset + + {/if} Delete diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index b10cd05e00..d039bb6eec 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -41,6 +41,7 @@ export function createAuthStore() { initials, isAdmin, isBuilder, + isSSO: !!$store.user?.provider, } }) diff --git a/packages/server/specs/openapi.json b/packages/server/specs/openapi.json index 9a0d69e352..cf4eef75a9 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -817,7 +817,6 @@ "type": "string", "enum": [ "string", - "barcodeqr", "longform", "options", "number", @@ -829,7 +828,8 @@ "formula", "auto", "json", - "internal" + "internal", + "barcodeqr" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, @@ -1021,7 +1021,6 @@ "type": "string", "enum": [ "string", - "barcodeqr", "longform", "options", "number", @@ -1033,7 +1032,8 @@ "formula", "auto", "json", - "internal" + "internal", + "barcodeqr" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, @@ -1236,7 +1236,6 @@ "type": "string", "enum": [ "string", - "barcodeqr", "longform", "options", "number", @@ -1248,7 +1247,8 @@ "formula", "auto", "json", - "internal" + "internal", + "barcodeqr" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index 69e44d881c..414efe7adb 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -603,7 +603,6 @@ components: type: string enum: - string - - barcodeqr - longform - options - number @@ -616,6 +615,7 @@ components: - auto - json - internal + - barcodeqr description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: @@ -766,7 +766,6 @@ components: type: string enum: - string - - barcodeqr - longform - options - number @@ -779,6 +778,7 @@ components: - auto - json - internal + - barcodeqr description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: @@ -936,7 +936,6 @@ components: type: string enum: - string - - barcodeqr - longform - options - number @@ -949,6 +948,7 @@ components: - auto - json - internal + - barcodeqr description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: diff --git a/packages/server/src/integrations/tests/couchdb.spec.ts b/packages/server/src/integrations/tests/couchdb.spec.ts index 0d744cd343..66735d7b74 100644 --- a/packages/server/src/integrations/tests/couchdb.spec.ts +++ b/packages/server/src/integrations/tests/couchdb.spec.ts @@ -1,5 +1,3 @@ -import { DatabaseWithConnection } from "@budibase/backend-core/src/db" - jest.mock("@budibase/backend-core", () => { const core = jest.requireActual("@budibase/backend-core") return { diff --git a/packages/types/src/api/account/index.ts b/packages/types/src/api/account/index.ts index 0cbc487bcc..4be610d1b3 100644 --- a/packages/types/src/api/account/index.ts +++ b/packages/types/src/api/account/index.ts @@ -1,2 +1,3 @@ export * from "./user" export * from "./license" +export * from "./status" diff --git a/packages/types/src/api/account/status.ts b/packages/types/src/api/account/status.ts new file mode 100644 index 0000000000..f6a0db7a5e --- /dev/null +++ b/packages/types/src/api/account/status.ts @@ -0,0 +1,7 @@ +export interface HealthStatusResponse { + passing: boolean + checks: { + login: boolean + search: boolean + } +} diff --git a/packages/types/src/api/web/auth.ts b/packages/types/src/api/web/auth.ts new file mode 100644 index 0000000000..e31a151c48 --- /dev/null +++ b/packages/types/src/api/web/auth.ts @@ -0,0 +1,25 @@ +export interface LoginRequest { + username: string + password: string +} + +export interface PasswordResetRequest { + email: string +} + +export interface PasswordResetUpdateRequest { + resetCode: string + password: string +} + +export interface UpdateSelfRequest { + firstName?: string + lastName?: string + password?: string + forceResetPassword?: boolean +} + +export interface UpdateSelfResponse { + _id: string + _rev: string +} diff --git a/packages/types/src/api/web/index.ts b/packages/types/src/api/web/index.ts index 9688a89c7b..8ed5b0aad4 100644 --- a/packages/types/src/api/web/index.ts +++ b/packages/types/src/api/web/index.ts @@ -1,4 +1,5 @@ export * from "./analytics" +export * from "./auth" export * from "./user" export * from "./errors" export * from "./schedule" diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 6acaf6912d..a435808f7e 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -1,6 +1,6 @@ import { User } from "../../documents" -export interface CreateUserResponse { +export interface SaveUserResponse { _id: string _rev: string email: string @@ -58,6 +58,25 @@ export interface CreateAdminUserRequest { tenantId: string } +export interface CreateAdminUserResponse { + _id: string + _rev: string + email: string +} + +export interface AcceptUserInviteRequest { + inviteCode: string + password: string + firstName: string + lastName: string +} + +export interface AcceptUserInviteResponse { + _id: string + _rev: string + email: string +} + export interface SyncUserRequest { previousUser?: User } diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index a8684f8427..cacdc4e9a2 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -79,14 +79,24 @@ export const isSelfHostAccount = (account: Account) => export const isSSOAccount = (account: Account): account is SSOAccount => account.authType === AuthType.SSO -export interface SSOAccount extends Account { - pictureUrl?: string - provider?: string - providerType?: string +export enum AccountSSOProviderType { + GOOGLE = "google", +} + +export enum AccountSSOProvider { + GOOGLE = "google", +} + +export interface AccountSSO { + provider: AccountSSOProvider + providerType: AccountSSOProviderType oauth2?: OAuthTokens + pictureUrl?: string thirdPartyProfile: any // TODO: define what the google profile looks like } +export type SSOAccount = (Account | CloudAccount) & AccountSSO + export enum AuthType { SSO = "sso", PASSWORD = "password", diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts index 9b05069c9a..99dec534b6 100644 --- a/packages/types/src/documents/global/config.ts +++ b/packages/types/src/documents/global/config.ts @@ -27,15 +27,17 @@ export interface SettingsConfig extends Config { } } -export interface GoogleConfig extends Config { - config: { - clientID: string - clientSecret: string - activated: boolean - } +export interface GoogleInnerConfig { + clientID: string + clientSecret: string + activated: boolean } -export interface OIDCConfiguration { +export interface GoogleConfig extends Config { + config: GoogleInnerConfig +} + +export interface OIDCStrategyConfiguration { issuer: string authorizationURL: string tokenURL: string @@ -45,7 +47,7 @@ export interface OIDCConfiguration { callbackURL: string } -export interface OIDCInnerCfg { +export interface OIDCInnerConfig { configUrl: string clientID: string clientSecret: string @@ -57,10 +59,17 @@ export interface OIDCInnerCfg { export interface OIDCConfig extends Config { config: { - configs: OIDCInnerCfg[] + configs: OIDCInnerConfig[] } } +export interface OIDCWellKnownConfig { + issuer: string + authorization_endpoint: string + token_endpoint: string + userinfo_endpoint: string +} + export const isSettingsConfig = (config: Config): config is SettingsConfig => config.type === ConfigType.SETTINGS diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index aef36c3469..2ef13a7412 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -1,37 +1,44 @@ import { Document } from "../document" -export interface SSOProfile { - id: string - name?: { - givenName?: string - familyName?: string - } - _json: { - email: string - picture: string - } - provider?: string +// SSO + +export interface SSOProfileJson { + email?: string + picture?: string } -export interface ThirdPartyUser extends Document { - thirdPartyProfile?: SSOProfile["_json"] - firstName?: string - lastName?: string - pictureUrl?: string - profile?: SSOProfile - oauth2?: any - provider?: string - providerType?: string - email: string - userId?: string - forceResetPassword?: boolean - userGroups?: string[] +export interface OAuth2 { + accessToken: string + refreshToken?: string } -export interface User extends ThirdPartyUser { +export enum SSOProviderType { + OIDC = "oidc", + GOOGLE = "google", +} + +export interface UserSSO { + provider: string // the individual provider e.g. Okta, Auth0, Google + providerType: SSOProviderType + oauth2?: OAuth2 + thirdPartyProfile?: SSOProfileJson +} + +export type SSOUser = User & UserSSO + +export function isSSOUser(user: User): user is SSOUser { + return !!(user as SSOUser).providerType +} + +// USER + +export interface User extends Document { tenantId: string email: string userId?: string + firstName?: string + lastName?: string + pictureUrl?: string forceResetPassword?: boolean roles: UserRoles builder?: { @@ -44,9 +51,7 @@ export interface User extends ThirdPartyUser { status?: string createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now() dayPassRecordedAt?: string - account?: { - authType: string - } + userGroups?: string[] onboardedAt?: string } @@ -54,7 +59,7 @@ export interface UserRoles { [key: string]: string } -// utility types +// UTILITY TYPES export interface BuilderUser extends User { builder: { diff --git a/packages/types/src/sdk/index.ts b/packages/types/src/sdk/index.ts index f8f9d9cb97..be12d45527 100644 --- a/packages/types/src/sdk/index.ts +++ b/packages/types/src/sdk/index.ts @@ -12,3 +12,5 @@ export * from "./db" export * from "./middleware" export * from "./featureFlag" export * from "./environmentVariables" +export * from "./sso" +export * from "./user" diff --git a/packages/types/src/sdk/sso.ts b/packages/types/src/sdk/sso.ts new file mode 100644 index 0000000000..2141204ccb --- /dev/null +++ b/packages/types/src/sdk/sso.ts @@ -0,0 +1,37 @@ +import { + OAuth2, + SSOProfileJson, + SSOProviderType, + SSOUser, + User, +} from "../documents" +import { SaveUserOpts } from "./user" + +export interface JwtClaims { + preferred_username?: string + email?: string +} + +export interface SSOAuthDetails { + oauth2: OAuth2 + provider: string + providerType: SSOProviderType + userId: string + email?: string + profile?: SSOProfile +} + +export interface SSOProfile { + id: string + name?: { + givenName?: string + familyName?: string + } + _json: SSOProfileJson + provider?: string +} + +export type SaveSSOUserFunction = ( + user: SSOUser, + opts: SaveUserOpts +) => Promise diff --git a/packages/types/src/sdk/user.ts b/packages/types/src/sdk/user.ts new file mode 100644 index 0000000000..1602eeb6c8 --- /dev/null +++ b/packages/types/src/sdk/user.ts @@ -0,0 +1,12 @@ +export interface UpdateSelf { + firstName?: string + lastName?: string + password?: string + forceResetPassword?: boolean +} + +export interface SaveUserOpts { + hashPassword?: boolean + requirePassword?: boolean + currentUserId?: string +} diff --git a/packages/worker/scripts/dev/manage.js b/packages/worker/scripts/dev/manage.js index 01cdef08ff..21a9eab9c6 100644 --- a/packages/worker/scripts/dev/manage.js +++ b/packages/worker/scripts/dev/manage.js @@ -29,6 +29,7 @@ async function init() { SERVICE: "worker-service", DEPLOYMENT_ENVIRONMENT: "development", TENANT_FEATURE_FLAGS: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR", + ENABLE_EMAIL_TEST_MODE: 1, } let envFile = "" Object.keys(envFileJson).forEach(key => { diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 738b67c553..948a98cf3a 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -1,49 +1,55 @@ import { - auth, + auth as authCore, constants, context, db as dbCore, events, tenancy, - users as usersCore, - utils, + utils as utilsCore, } from "@budibase/backend-core" -import { EmailTemplatePurpose } from "../../../constants" -import { isEmailConfigured, sendEmail } from "../../../utilities/email" -import { checkResetPasswordCode } from "../../../utilities/redis" +import { + ConfigType, + User, + Ctx, + LoginRequest, + SSOUser, + PasswordResetRequest, + PasswordResetUpdateRequest, +} from "@budibase/types" import env from "../../../environment" -import sdk from "../../../sdk" -import { ConfigType, User } from "@budibase/types" -const { setCookie, getCookie, clearCookie, hash, platformLogout } = utils +import * as authSdk from "../../../sdk/auth" +import * as userSdk from "../../../sdk/users" + const { Cookie, Header } = constants -const { passport, ssoCallbackUrl, google, oidc } = auth +const { passport, ssoCallbackUrl, google, oidc } = authCore +const { setCookie, getCookie, clearCookie } = utilsCore -export async function googleCallbackUrl(config?: { callbackURL?: string }) { - return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.GOOGLE) -} +// LOGIN / LOGOUT -export async function oidcCallbackUrl(config?: { callbackURL?: string }) { - return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.OIDC) -} - -async function authInternal(ctx: any, user: any, err: any = null, info = null) { +async function passportCallback( + ctx: Ctx, + user: User, + err: any = null, + info: { message: string } | null = null +) { if (err) { console.error("Authentication error") console.error(err) console.trace(err) return ctx.throw(403, info ? info : "Unauthorized") } - if (!user) { console.error("Authentication error - no user provided") return ctx.throw(403, info ? info : "Unauthorized") } + const token = await authSdk.loginUser(user) + // set a cookie for browser access - setCookie(ctx, user.token, Cookie.Auth, { sign: false }) + setCookie(ctx, token, Cookie.Auth, { sign: false }) // set the token in a header as well for APIs - ctx.set(Header.TOKEN, user.token) + ctx.set(Header.TOKEN, token) // get rid of any app cookies on login // have to check test because this breaks cypress if (!env.isTest()) { @@ -51,11 +57,18 @@ async function authInternal(ctx: any, user: any, err: any = null, info = null) { } } -export const authenticate = async (ctx: any, next: any) => { +export const login = async (ctx: Ctx, next: any) => { + const email = ctx.request.body.username + + const user = await userSdk.getUserByEmail(email) + if (user && (await userSdk.isPreventSSOPasswords(user))) { + ctx.throw(400, "SSO user cannot login using password") + } + return passport.authenticate( "local", async (err: any, user: User, info: any) => { - await authInternal(ctx, user, err, info) + await passportCallback(ctx, user, err, info) await context.identity.doInUserContext(user, async () => { await events.auth.login("local") }) @@ -64,6 +77,15 @@ export const authenticate = async (ctx: any, next: any) => { )(ctx, next) } +export const logout = async (ctx: any) => { + if (ctx.user && ctx.user._id) { + await authSdk.logout({ ctx, userId: ctx.user._id }) + } + ctx.body = { message: "User logged out." } +} + +// INIT + export const setInitInfo = (ctx: any) => { const initInfo = ctx.request.body setCookie(ctx, initInfo, Cookie.Init) @@ -79,32 +101,16 @@ export const getInitInfo = (ctx: any) => { } } +// PASSWORD MANAGEMENT + /** * Reset the user password, used as part of a forgotten password flow. */ -export const reset = async (ctx: any) => { +export const reset = async (ctx: Ctx) => { const { email } = ctx.request.body - const configured = await isEmailConfigured() - if (!configured) { - ctx.throw( - 400, - "Please contact your platform administrator, SMTP is not configured." - ) - } - try { - const user = (await usersCore.getGlobalUserByEmail(email)) as User - // only if user exists, don't error though if they don't - if (user) { - await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { - user, - subject: "{{ company }} platform password reset", - }) - await events.user.passwordResetRequested(user) - } - } catch (err) { - console.log(err) - // don't throw any kind of error to the user, this might give away something - } + + await authSdk.reset(email) + ctx.body = { message: "Please check your email for a reset link.", } @@ -113,32 +119,21 @@ export const reset = async (ctx: any) => { /** * Perform the user password update if the provided reset code is valid. */ -export const resetUpdate = async (ctx: any) => { +export const resetUpdate = async (ctx: Ctx) => { const { resetCode, password } = ctx.request.body try { - const { userId } = await checkResetPasswordCode(resetCode) - const db = tenancy.getGlobalDB() - const user = await db.get(userId) - user.password = await hash(password) - await db.put(user) + await authSdk.resetUpdate(resetCode, password) ctx.body = { message: "password reset successfully.", } - // remove password from the user before sending events - delete user.password - await events.user.passwordReset(user) } catch (err) { - console.error(err) + console.warn(err) + // hide any details of the error for security ctx.throw(400, "Cannot reset password.") } } -export const logout = async (ctx: any) => { - if (ctx.user && ctx.user._id) { - await platformLogout({ ctx, userId: ctx.user._id }) - } - ctx.body = { message: "User logged out." } -} +// DATASOURCE export const datasourcePreAuth = async (ctx: any, next: any) => { const provider = ctx.params.provider @@ -166,6 +161,12 @@ export const datasourceAuth = async (ctx: any, next: any) => { return handler.postAuth(passport, ctx, next) } +// GOOGLE SSO + +export async function googleCallbackUrl(config?: { callbackURL?: string }) { + return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.GOOGLE) +} + /** * The initial call that google authentication makes to take you to the google login screen. * On a successful login, you will be redirected to the googleAuth callback route. @@ -181,7 +182,7 @@ export const googlePreAuth = async (ctx: any, next: any) => { const strategy = await google.strategyFactory( config, callbackUrl, - sdk.users.save + userSdk.save ) return passport.authenticate(strategy, { @@ -191,7 +192,7 @@ export const googlePreAuth = async (ctx: any, next: any) => { })(ctx, next) } -export const googleAuth = async (ctx: any, next: any) => { +export const googleCallback = async (ctx: any, next: any) => { const db = tenancy.getGlobalDB() const config = await dbCore.getScopedConfig(db, { @@ -202,14 +203,14 @@ export const googleAuth = async (ctx: any, next: any) => { const strategy = await google.strategyFactory( config, callbackUrl, - sdk.users.save + userSdk.save ) return passport.authenticate( strategy, { successRedirect: "/", failureRedirect: "/error" }, - async (err: any, user: User, info: any) => { - await authInternal(ctx, user, err, info) + async (err: any, user: SSOUser, info: any) => { + await passportCallback(ctx, user, err, info) await context.identity.doInUserContext(user, async () => { await events.auth.login("google-internal") }) @@ -218,6 +219,12 @@ export const googleAuth = async (ctx: any, next: any) => { )(ctx, next) } +// OIDC SSO + +export async function oidcCallbackUrl(config?: { callbackURL?: string }) { + return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.OIDC) +} + export const oidcStrategyFactory = async (ctx: any, configId: any) => { const db = tenancy.getGlobalDB() const config = await dbCore.getScopedConfig(db, { @@ -233,7 +240,7 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => { chosenConfig, callbackUrl ) - return oidc.strategyFactory(enrichedConfig, sdk.users.save) + return oidc.strategyFactory(enrichedConfig, userSdk.save) } /** @@ -265,15 +272,15 @@ export const oidcPreAuth = async (ctx: any, next: any) => { })(ctx, next) } -export const oidcAuth = async (ctx: any, next: any) => { +export const oidcCallback = async (ctx: any, next: any) => { const configId = getCookie(ctx, Cookie.OIDC_CONFIG) const strategy = await oidcStrategyFactory(ctx, configId) return passport.authenticate( strategy, { successRedirect: "/", failureRedirect: "/error" }, - async (err: any, user: any, info: any) => { - await authInternal(ctx, user, err, info) + async (err: any, user: SSOUser, info: any) => { + await passportCallback(ctx, user, err, info) await context.identity.doInUserContext(user, async () => { await events.auth.login("oidc") }) diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts index 06906f1e8e..889a3e6a27 100644 --- a/packages/worker/src/api/controllers/global/self.ts +++ b/packages/worker/src/api/controllers/global/self.ts @@ -1,18 +1,22 @@ -import sdk from "../../../sdk" +import * as userSdk from "../../../sdk/users" import { - events, featureFlags, tenancy, constants, db as dbCore, utils, - cache, encryption, + auth as authCore, } from "@budibase/backend-core" import env from "../../../environment" import { groups } from "@budibase/pro" -const { hash, platformLogout, getCookie, clearCookie, newid } = utils -const { user: userCache } = cache +import { + UpdateSelfRequest, + UpdateSelfResponse, + UpdateSelf, + UserCtx, +} from "@budibase/types" +const { getCookie, clearCookie, newid } = utils function newTestApiKey() { return env.ENCRYPTED_TEST_PUBLIC_API_KEY @@ -93,17 +97,6 @@ const addSessionAttributesToUser = (ctx: any) => { ctx.body.csrfToken = ctx.user.csrfToken } -const sanitiseUserUpdate = (ctx: any) => { - const allowed = ["firstName", "lastName", "password", "forceResetPassword"] - const resp: { [key: string]: any } = {} - for (let [key, value] of Object.entries(ctx.request.body)) { - if (allowed.includes(key)) { - resp[key] = value - } - } - return resp -} - export async function getSelf(ctx: any) { if (!ctx.user) { ctx.throw(403, "User not logged in") @@ -116,7 +109,7 @@ export async function getSelf(ctx: any) { checkCurrentApp(ctx) // get the main body of the user - const user = await sdk.users.getUser(userId) + const user = await userSdk.getUser(userId) ctx.body = await groups.enrichUserRolesFromGroups(user) // add the feature flags for this tenant @@ -126,39 +119,30 @@ export async function getSelf(ctx: any) { addSessionAttributesToUser(ctx) } -export async function updateSelf(ctx: any) { - const db = tenancy.getGlobalDB() - const user = await db.get(ctx.user._id) - let passwordChange = false +export async function updateSelf( + ctx: UserCtx +) { + const body = ctx.request.body + const update: UpdateSelf = { + firstName: body.firstName, + lastName: body.lastName, + password: body.password, + forceResetPassword: body.forceResetPassword, + } - const userUpdateObj = sanitiseUserUpdate(ctx) - if (userUpdateObj.password) { - // changing password - passwordChange = true - userUpdateObj.password = await hash(userUpdateObj.password) + const user = await userSdk.updateSelf(ctx.user._id!, update) + + if (update.password) { // Log all other sessions out apart from the current one - await platformLogout({ + await authCore.platformLogout({ ctx, - userId: ctx.user._id, + userId: ctx.user._id!, keepActiveSession: true, }) } - const response = await db.put({ - ...user, - ...userUpdateObj, - }) - await userCache.invalidateUser(user._id) ctx.body = { - _id: response.id, - _rev: response.rev, - } - - // remove the old password from the user before sending events - user._rev = response.rev - delete user.password - await events.user.updated(user) - if (passwordChange) { - await events.user.passwordUpdated(user) + _id: user._id!, + _rev: user._rev!, } } diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 43ec23eade..c722d27faa 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -2,15 +2,21 @@ import { checkInviteCode } from "../../../utilities/redis" import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { + AcceptUserInviteRequest, + AcceptUserInviteResponse, BulkUserRequest, BulkUserResponse, CloudAccount, CreateAdminUserRequest, + CreateAdminUserResponse, + Ctx, InviteUserRequest, InviteUsersRequest, MigrationType, + SaveUserResponse, SearchUsersRequest, User, + UserCtx, } from "@budibase/types" import { accounts, @@ -25,10 +31,18 @@ import { checkAnyUserExists } from "../../../utilities/users" const MAX_USERS_UPLOAD_LIMIT = 1000 -export const save = async (ctx: any) => { +export const save = async (ctx: UserCtx) => { try { const currentUserId = ctx.user._id - ctx.body = await userSdk.save(ctx.request.body, { currentUserId }) + const requestUser = ctx.request.body + + const user = await userSdk.save(requestUser, { currentUserId }) + + ctx.body = { + _id: user._id!, + _rev: user._rev!, + email: user.email, + } } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -71,9 +85,10 @@ const parseBooleanParam = (param: any) => { return !(param && param === "false") } -export const adminUser = async (ctx: any) => { - const { email, password, tenantId } = ctx.request - .body as CreateAdminUserRequest +export const adminUser = async ( + ctx: Ctx +) => { + const { email, password, tenantId } = ctx.request.body if (await platform.tenants.exists(tenantId)) { ctx.throw(403, "Organisation already exists.") @@ -131,7 +146,11 @@ export const adminUser = async (ctx: any) => { } await events.identification.identifyTenantGroup(tenantId, account) - ctx.body = finalUser + ctx.body = { + _id: finalUser._id!, + _rev: finalUser._rev!, + email: finalUser.email, + } } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -236,12 +255,14 @@ export const checkInvite = async (ctx: any) => { } } -export const inviteAccept = async (ctx: any) => { +export const inviteAccept = async ( + ctx: Ctx +) => { const { inviteCode, password, firstName, lastName } = ctx.request.body try { // info is an extension of the user object that was stored by global const { email, info }: any = await checkInviteCode(inviteCode) - ctx.body = await tenancy.doInTenant(info.tenantId, async () => { + const user = await tenancy.doInTenant(info.tenantId, async () => { const saved = await userSdk.save({ firstName, lastName, @@ -254,6 +275,12 @@ export const inviteAccept = async (ctx: any) => { await events.user.inviteAccepted(user) return saved }) + + ctx.body = { + _id: user._id, + _rev: user._rev, + email: user.email, + } } catch (err: any) { if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) { // explicitly re-throw limit exceeded errors diff --git a/packages/worker/src/api/controllers/system/accounts.ts b/packages/worker/src/api/controllers/system/accounts.ts index 0aa5f25785..10b809c0b5 100644 --- a/packages/worker/src/api/controllers/system/accounts.ts +++ b/packages/worker/src/api/controllers/system/accounts.ts @@ -1,21 +1,23 @@ import { Account, AccountMetadata } from "@budibase/types" -import sdk from "../../../sdk" +import * as accounts from "../../../sdk/accounts" export const save = async (ctx: any) => { const account = ctx.request.body as Account let metadata: AccountMetadata = { - _id: sdk.accounts.formatAccountMetadataId(account.accountId), + _id: accounts.metadata.formatAccountMetadataId(account.accountId), email: account.email, } - metadata = await sdk.accounts.saveMetadata(metadata) + metadata = await accounts.metadata.saveMetadata(metadata) ctx.body = metadata ctx.status = 200 } export const destroy = async (ctx: any) => { - const accountId = sdk.accounts.formatAccountMetadataId(ctx.params.accountId) - await sdk.accounts.destroyMetadata(accountId) + const accountId = accounts.metadata.formatAccountMetadataId( + ctx.params.accountId + ) + await accounts.metadata.destroyMetadata(accountId) ctx.status = 204 } diff --git a/packages/worker/src/api/routes/global/auth.ts b/packages/worker/src/api/routes/global/auth.ts index b13cef2fc6..503182a418 100644 --- a/packages/worker/src/api/routes/global/auth.ts +++ b/packages/worker/src/api/routes/global/auth.ts @@ -33,7 +33,7 @@ router .post( "/api/global/auth/:tenantId/login", buildAuthValidation(), - authController.authenticate + authController.login ) .post("/api/global/auth/logout", authController.logout) .post( @@ -68,21 +68,24 @@ router // GOOGLE - MULTI TENANT .get("/api/global/auth/:tenantId/google", authController.googlePreAuth) - .get("/api/global/auth/:tenantId/google/callback", authController.googleAuth) + .get( + "/api/global/auth/:tenantId/google/callback", + authController.googleCallback + ) // GOOGLE - SINGLE TENANT - DEPRECATED - .get("/api/global/auth/google/callback", authController.googleAuth) - .get("/api/admin/auth/google/callback", authController.googleAuth) + .get("/api/global/auth/google/callback", authController.googleCallback) + .get("/api/admin/auth/google/callback", authController.googleCallback) // OIDC - MULTI TENANT .get( "/api/global/auth/:tenantId/oidc/configs/:configId", authController.oidcPreAuth ) - .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth) + .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcCallback) // OIDC - SINGLE TENANT - DEPRECATED - .get("/api/global/auth/oidc/callback", authController.oidcAuth) - .get("/api/admin/auth/oidc/callback", authController.oidcAuth) + .get("/api/global/auth/oidc/callback", authController.oidcCallback) + .get("/api/admin/auth/oidc/callback", authController.oidcCallback) export default router diff --git a/packages/worker/src/api/routes/global/tests/auth.spec.ts b/packages/worker/src/api/routes/global/tests/auth.spec.ts index ee753d49dc..84f8ce1b0a 100644 --- a/packages/worker/src/api/routes/global/tests/auth.spec.ts +++ b/packages/worker/src/api/routes/global/tests/auth.spec.ts @@ -1,13 +1,27 @@ -jest.mock("nodemailer") -import { TestConfiguration, mocks } from "../../../../tests" -const sendMailMock = mocks.email.mock() -import { events, tenancy } from "@budibase/backend-core" -import { structures } from "@budibase/backend-core/tests" +import { CloudAccount, SSOUser, User } from "@budibase/types" -const expectSetAuthCookie = (res: any) => { - expect( - res.get("Set-Cookie").find((c: string) => c.startsWith("budibase:auth")) - ).toBeDefined() +jest.mock("nodemailer") +import { + TestConfiguration, + mocks, + structures, + generator, +} from "../../../../tests" +const sendMailMock = mocks.email.mock() +import { events, constants } from "@budibase/backend-core" +import { Response } from "superagent" + +import * as userSdk from "../../../../sdk/users" + +function getAuthCookie(response: Response) { + return response.headers["set-cookie"] + .find((s: string) => s.startsWith(`${constants.Cookie.Auth}=`)) + .split("=")[1] + .split(";")[0] +} + +const expectSetAuthCookie = (response: Response) => { + expect(getAuthCookie(response).length > 1).toBe(true) } describe("/api/global/auth", () => { @@ -25,60 +39,247 @@ describe("/api/global/auth", () => { jest.clearAllMocks() }) + async function createSSOUser() { + return config.doInTenant(async () => { + return userSdk.save(structures.users.ssoUser(), { + requirePassword: false, + }) + }) + } + describe("password", () => { describe("POST /api/global/auth/:tenantId/login", () => { - it("should login", () => {}) + it("logs in with correct credentials", async () => { + const tenantId = config.tenantId! + const email = config.user?.email! + const password = config.userPassword + + const response = await config.api.auth.login(tenantId, email, password) + + expectSetAuthCookie(response) + expect(events.auth.login).toBeCalledTimes(1) + }) + + it("should return 403 with incorrect credentials", async () => { + const tenantId = config.tenantId! + const email = config.user?.email! + const password = "incorrect" + + const response = await config.api.auth.login( + tenantId, + email, + password, + { status: 403 } + ) + expect(response.body).toEqual({ + message: "Invalid credentials", + status: 403, + }) + }) + + it("should return 403 when user doesn't exist", async () => { + const tenantId = config.tenantId! + const email = "invaliduser@test.com" + const password = "password" + + const response = await config.api.auth.login( + tenantId, + email, + password, + { status: 403 } + ) + expect(response.body).toEqual({ + message: "Invalid credentials", + status: 403, + }) + }) + + describe("sso user", () => { + let user: User + + async function testSSOUser() { + const tenantId = user.tenantId! + const email = user.email + const password = "test" + + const response = await config.api.auth.login( + tenantId, + email, + password, + { status: 400 } + ) + + expect(response.body).toEqual({ + message: "SSO user cannot login using password", + status: 400, + }) + } + + describe("budibase sso user", () => { + it("should prevent user from logging in", async () => { + user = await createSSOUser() + await testSSOUser() + }) + }) + + describe("root account sso user", () => { + it("should prevent user from logging in", async () => { + user = await config.createUser() + const account = structures.accounts.ssoAccount() as CloudAccount + mocks.accounts.getAccount.mockReturnValueOnce( + Promise.resolve(account) + ) + + await testSSOUser() + }) + }) + }) }) describe("POST /api/global/auth/logout", () => { it("should logout", async () => { - await config.api.auth.logout() + const response = await config.api.auth.logout() expect(events.auth.logout).toBeCalledTimes(1) - // TODO: Verify sessions deleted + const authCookie = getAuthCookie(response) + expect(authCookie).toBe("") }) }) describe("POST /api/global/auth/:tenantId/reset", () => { it("should generate password reset email", async () => { - await tenancy.doInTenant(config.tenant1User!.tenantId, async () => { - const userEmail = structures.email() - const { res, code } = await config.api.auth.requestPasswordReset( + const user = await config.createUser() + + const { res, code } = await config.api.auth.requestPasswordReset( + sendMailMock, + user.email + ) + + expect(res.body).toEqual({ + message: "Please check your email for a reset link.", + }) + expect(sendMailMock).toHaveBeenCalled() + expect(code).toBeDefined() + expect(events.user.passwordResetRequested).toBeCalledTimes(1) + expect(events.user.passwordResetRequested).toBeCalledWith(user) + }) + + describe("sso user", () => { + let user: User + + async function testSSOUser() { + const { res } = await config.api.auth.requestPasswordReset( sendMailMock, - userEmail + user.email, + { status: 400 } ) - const user = await config.getUser(userEmail) expect(res.body).toEqual({ - message: "Please check your email for a reset link.", + message: "SSO user cannot reset password", + status: 400, + error: { + code: "http", + type: "generic", + }, }) - expect(sendMailMock).toHaveBeenCalled() + expect(sendMailMock).not.toHaveBeenCalled() + } - expect(code).toBeDefined() - expect(events.user.passwordResetRequested).toBeCalledTimes(1) - expect(events.user.passwordResetRequested).toBeCalledWith(user) + describe("budibase sso user", () => { + it("should prevent user from generating password reset email", async () => { + user = await createSSOUser() + await testSSOUser() + }) + }) + + describe("root account sso user", () => { + it("should prevent user from generating password reset email", async () => { + user = await config.createUser(structures.users.user()) + const account = structures.accounts.ssoAccount() as CloudAccount + mocks.accounts.getAccount.mockReturnValueOnce( + Promise.resolve(account) + ) + + await testSSOUser() + }) }) }) }) describe("POST /api/global/auth/:tenantId/reset/update", () => { it("should reset password", async () => { - await tenancy.doInTenant(config.tenant1User!.tenantId, async () => { - const userEmail = structures.email() - const { code } = await config.api.auth.requestPasswordReset( - sendMailMock, - userEmail + let user = await config.createUser() + const { code } = await config.api.auth.requestPasswordReset( + sendMailMock, + user.email + ) + delete user.password + + const newPassword = "newpassword" + const res = await config.api.auth.updatePassword(code!, newPassword) + + user = await config.getUser(user.email) + delete user.password + + expect(res.body).toEqual({ message: "password reset successfully." }) + expect(events.user.passwordReset).toBeCalledTimes(1) + expect(events.user.passwordReset).toBeCalledWith(user) + + // login using new password + await config.api.auth.login(user.tenantId, user.email, newPassword) + }) + + describe("sso user", () => { + let user: User | SSOUser + + async function testSSOUser(code: string) { + const res = await config.api.auth.updatePassword( + code!, + generator.string(), + { status: 400 } ) - const user = await config.getUser(userEmail) - delete user.password - const res = await config.api.auth.updatePassword(code) + expect(res.body).toEqual({ + message: "Cannot reset password.", + status: 400, + }) + } - expect(res.body).toEqual({ message: "password reset successfully." }) - expect(events.user.passwordReset).toBeCalledTimes(1) - expect(events.user.passwordReset).toBeCalledWith(user) + describe("budibase sso user", () => { + it("should prevent user from generating password reset email", async () => { + user = await config.createUser() + const { code } = await config.api.auth.requestPasswordReset( + sendMailMock, + user.email + ) + + // convert to sso now that password reset has been requested + const ssoUser = user as SSOUser + ssoUser.providerType = structures.sso.providerType() + delete ssoUser.password + await config.doInTenant(() => userSdk.save(ssoUser)) + + await testSSOUser(code!) + }) + }) + + describe("root account sso user", () => { + it("should prevent user from generating password reset email", async () => { + user = await config.createUser() + const { code } = await config.api.auth.requestPasswordReset( + sendMailMock, + user.email + ) + + // convert to account owner now that password has been requested + const account = structures.accounts.ssoAccount() as CloudAccount + mocks.accounts.getAccount.mockReturnValueOnce( + Promise.resolve(account) + ) + + await testSSOUser(code!) + }) }) - // TODO: Login using new password }) }) }) @@ -153,7 +354,7 @@ describe("/api/global/auth", () => { const location: string = res.get("location") expect( location.startsWith( - "http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2Fdefault%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access" + `http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2F${config.tenantId}%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access` ) ).toBe(true) }) diff --git a/packages/worker/src/api/routes/global/tests/self.spec.ts b/packages/worker/src/api/routes/global/tests/self.spec.ts index d253a7f24e..74d24c7c31 100644 --- a/packages/worker/src/api/routes/global/tests/self.spec.ts +++ b/packages/worker/src/api/routes/global/tests/self.spec.ts @@ -30,7 +30,7 @@ describe("/api/global/self", () => { user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString() expect(res.body._id).toBe(user._id) expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.updated).toBeCalledWith(user) + expect(events.user.updated).toBeCalledWith(dbUser) expect(events.user.passwordUpdated).not.toBeCalled() }) @@ -44,12 +44,11 @@ describe("/api/global/self", () => { const dbUser = await config.getUser(user.email) user._rev = dbUser._rev user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString() - delete user.password expect(res.body._id).toBe(user._id) expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.updated).toBeCalledWith(user) + expect(events.user.updated).toBeCalledWith(dbUser) expect(events.user.passwordUpdated).toBeCalledTimes(1) - expect(events.user.passwordUpdated).toBeCalledWith(user) + expect(events.user.passwordUpdated).toBeCalledWith(dbUser) }) }) }) diff --git a/packages/worker/src/api/routes/system/tests/accounts.spec.ts b/packages/worker/src/api/routes/system/tests/accounts.spec.ts index fd54dd2b0a..7df2950212 100644 --- a/packages/worker/src/api/routes/system/tests/accounts.spec.ts +++ b/packages/worker/src/api/routes/system/tests/accounts.spec.ts @@ -1,4 +1,4 @@ -import sdk from "../../../../sdk" +import * as accounts from "../../../../sdk/accounts" import { TestConfiguration, structures } from "../../../../tests" import { v4 as uuid } from "uuid" @@ -24,8 +24,8 @@ describe("accounts", () => { const response = await config.api.accounts.saveMetadata(account) - const id = sdk.accounts.formatAccountMetadataId(account.accountId) - const metadata = await sdk.accounts.getMetadata(id) + const id = accounts.metadata.formatAccountMetadataId(account.accountId) + const metadata = await accounts.metadata.getMetadata(id) expect(response).toStrictEqual(metadata) }) }) @@ -37,7 +37,7 @@ describe("accounts", () => { await config.api.accounts.destroyMetadata(account.accountId) - const deleted = await sdk.accounts.getMetadata(account.accountId) + const deleted = await accounts.metadata.getMetadata(account.accountId) expect(deleted).toBe(undefined) }) diff --git a/packages/worker/src/environment.ts b/packages/worker/src/environment.ts index 52fec210bc..71fd89f276 100644 --- a/packages/worker/src/environment.ts +++ b/packages/worker/src/environment.ts @@ -26,6 +26,8 @@ function parseIntSafe(number: any) { } } +const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") + const environment = { // auth MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, @@ -49,7 +51,7 @@ const environment = { CLUSTER_PORT: process.env.CLUSTER_PORT, // flags NODE_ENV: process.env.NODE_ENV, - SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), + SELF_HOSTED: selfHosted, LOG_LEVEL: process.env.LOG_LEVEL, MULTI_TENANCY: process.env.MULTI_TENANCY, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, @@ -65,6 +67,18 @@ const environment = { CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600, SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY, + /** + * Mock the email service in use - links to ethereal hosted emails are logged instead. + */ + ENABLE_EMAIL_TEST_MODE: process.env.ENABLE_EMAIL_TEST_MODE, + /** + * Enable to allow an admin user to login using a password. + * This can be useful to prevent lockout when configuring SSO. + * However, this should be turned OFF by default for security purposes. + */ + ENABLE_SSO_MAINTENANCE_MODE: selfHosted + ? process.env.ENABLE_SSO_MAINTENANCE_MODE + : false, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 1eff6c06fb..1e3ff3cbdf 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -25,6 +25,12 @@ const koaSession = require("koa-session") const logger = require("koa-pino-logger") import destroyable from "server-destroy" +if (env.ENABLE_SSO_MAINTENANCE_MODE) { + console.warn( + "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress" + ) +} + // this will setup http and https proxies form env variables bootstrap() diff --git a/packages/worker/src/sdk/accounts/index.ts b/packages/worker/src/sdk/accounts/index.ts index f2ae03040e..72db8c3d85 100644 --- a/packages/worker/src/sdk/accounts/index.ts +++ b/packages/worker/src/sdk/accounts/index.ts @@ -1 +1,2 @@ -export * from "./accounts" +export * as metadata from "./metadata" +export { accounts as api } from "@budibase/backend-core" diff --git a/packages/worker/src/sdk/accounts/accounts.ts b/packages/worker/src/sdk/accounts/metadata.ts similarity index 99% rename from packages/worker/src/sdk/accounts/accounts.ts rename to packages/worker/src/sdk/accounts/metadata.ts index e43285087b..64065e8b78 100644 --- a/packages/worker/src/sdk/accounts/accounts.ts +++ b/packages/worker/src/sdk/accounts/metadata.ts @@ -2,7 +2,6 @@ import { AccountMetadata } from "@budibase/types" import { db, StaticDatabases, - HTTPError, DocumentType, SEPARATOR, } from "@budibase/backend-core" diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts new file mode 100644 index 0000000000..15a4f3c7e7 --- /dev/null +++ b/packages/worker/src/sdk/auth/auth.ts @@ -0,0 +1,86 @@ +import { + auth as authCore, + tenancy, + utils as coreUtils, + sessions, + events, + HTTPError, +} from "@budibase/backend-core" +import { PlatformLogoutOpts, User } from "@budibase/types" +import jwt from "jsonwebtoken" +import env from "../../environment" +import * as userSdk from "../users" +import * as emails from "../../utilities/email" +import * as redis from "../../utilities/redis" +import { EmailTemplatePurpose } from "../../constants" + +// LOGIN / LOGOUT + +export async function loginUser(user: User) { + const sessionId = coreUtils.newid() + const tenantId = tenancy.getTenantId() + await sessions.createASession(user._id!, { sessionId, tenantId }) + const token = jwt.sign( + { + userId: user._id, + sessionId, + tenantId, + }, + env.JWT_SECRET! + ) + return token +} + +export async function logout(opts: PlatformLogoutOpts) { + // TODO: This should be moved out of core and into worker only + // account-portal can call worker endpoint + return authCore.platformLogout(opts) +} + +// PASSWORD MANAGEMENT + +/** + * Reset the user password, used as part of a forgotten password flow. + */ +export const reset = async (email: string) => { + const configured = await emails.isEmailConfigured() + if (!configured) { + throw new HTTPError( + "Please contact your platform administrator, SMTP is not configured.", + 400 + ) + } + + const user = await userSdk.core.getGlobalUserByEmail(email) + // exit if user doesn't exist + if (!user) { + return + } + + // exit if user has sso + if (await userSdk.isPreventSSOPasswords(user)) { + throw new HTTPError("SSO user cannot reset password", 400) + } + + // send password reset + await emails.sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { + user, + subject: "{{ company }} platform password reset", + }) + await events.user.passwordResetRequested(user) +} + +/** + * Perform the user password update if the provided reset code is valid. + */ +export const resetUpdate = async (resetCode: string, password: string) => { + const { userId } = await redis.checkResetPasswordCode(resetCode) + + let user = await userSdk.getUser(userId) + user.password = password + user = await userSdk.save(user) + + // remove password from the user before sending events + delete user.password + await events.user.passwordReset(user) +} diff --git a/packages/worker/src/sdk/auth/index.ts b/packages/worker/src/sdk/auth/index.ts new file mode 100644 index 0000000000..306751af96 --- /dev/null +++ b/packages/worker/src/sdk/auth/index.ts @@ -0,0 +1 @@ +export * from "./auth" diff --git a/packages/worker/src/sdk/users/events.ts b/packages/worker/src/sdk/users/events.ts index 17c4748dff..7d86182a3c 100644 --- a/packages/worker/src/sdk/users/events.ts +++ b/packages/worker/src/sdk/users/events.ts @@ -84,6 +84,10 @@ export const handleSaveEvents = async ( ) { await events.user.passwordForceReset(user) } + + if (user.password !== existingUser.password) { + await events.user.passwordUpdated(user) + } } else { await events.user.created(user) } diff --git a/packages/worker/src/sdk/users/index.ts b/packages/worker/src/sdk/users/index.ts index 056d6e5675..2eaa0e68a2 100644 --- a/packages/worker/src/sdk/users/index.ts +++ b/packages/worker/src/sdk/users/index.ts @@ -1 +1,2 @@ export * from "./users" +export { users as core } from "@budibase/backend-core" diff --git a/packages/worker/src/sdk/users/tests/users.spec.ts b/packages/worker/src/sdk/users/tests/users.spec.ts new file mode 100644 index 0000000000..41d9298997 --- /dev/null +++ b/packages/worker/src/sdk/users/tests/users.spec.ts @@ -0,0 +1,52 @@ +import { structures } from "../../../tests" +import * as users from "../users" +import env from "../../../environment" +import { mocks } from "@budibase/backend-core/tests" +import { CloudAccount } from "@budibase/types" + +describe("users", () => { + describe("isPreventSSOPasswords", () => { + it("returns true for sso account user", async () => { + const user = structures.users.user() + mocks.accounts.getAccount.mockReturnValue( + Promise.resolve(structures.accounts.ssoAccount() as CloudAccount) + ) + const result = await users.isPreventSSOPasswords(user) + expect(result).toBe(true) + }) + + it("returns true for sso user", async () => { + const user = structures.users.ssoUser() + const result = await users.isPreventSSOPasswords(user) + expect(result).toBe(true) + }) + + describe("sso maintenance mode", () => { + beforeEach(() => { + env._set("ENABLE_SSO_MAINTENANCE_MODE", true) + }) + + afterEach(() => { + env._set("ENABLE_SSO_MAINTENANCE_MODE", false) + }) + + describe("non-admin user", () => { + it("returns true", async () => { + const user = structures.users.ssoUser() + const result = await users.isPreventSSOPasswords(user) + expect(result).toBe(true) + }) + }) + + describe("admin user", () => { + it("returns false", async () => { + const user = structures.users.ssoUser({ + user: structures.users.adminUser(), + }) + const result = await users.isPreventSSOPasswords(user) + expect(result).toBe(false) + }) + }) + }) + }) +}) diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 1d05f6d84f..7d4a2f04f0 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -6,12 +6,11 @@ import { cache, constants, db as dbUtils, - deprovisioning, events, HTTPError, - migrations, sessions, tenancy, + platform, users as usersCore, utils, ViewName, @@ -21,21 +20,22 @@ import { AllDocsResponse, BulkUserResponse, CloudAccount, - CreateUserResponse, InviteUsersRequest, InviteUsersResponse, - MigrationType, + isSSOAccount, + isSSOUser, PlatformUser, PlatformUserByEmail, RowResponse, SearchUsersRequest, + UpdateSelf, User, - ThirdPartyUser, - isUser, + SaveUserOpts, } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" import { groups as groupsSdk } from "@budibase/pro" +import * as accountSdk from "../accounts" const PAGE_LIMIT = 8 @@ -94,26 +94,23 @@ export const paginatedUsers = async ({ }) } +export async function getUserByEmail(email: string) { + return usersCore.getGlobalUserByEmail(email) +} + /** * Gets a user by ID from the global database, based on the current tenancy. */ export const getUser = async (userId: string) => { - const db = tenancy.getGlobalDB() - let user = await db.get(userId) + const user = await usersCore.getById(userId) if (user) { delete user.password } return user } -export interface SaveUserOpts { - hashPassword?: boolean - requirePassword?: boolean - currentUserId?: string -} - const buildUser = async ( - user: User | ThirdPartyUser, + user: User, opts: SaveUserOpts = { hashPassword: true, requirePassword: true, @@ -121,11 +118,13 @@ const buildUser = async ( tenantId: string, dbUser?: any ): Promise => { - let fullUser = user as User - let { password, _id } = fullUser + let { password, _id } = user let hashedPassword if (password) { + if (await isPreventSSOPasswords(user)) { + throw new HTTPError("SSO user cannot set password", 400) + } hashedPassword = opts.hashPassword ? await utils.hash(password) : password } else if (dbUser) { hashedPassword = dbUser.password @@ -135,10 +134,10 @@ const buildUser = async ( _id = _id || dbUtils.generateGlobalUserID() - fullUser = { + const fullUser = { createdAt: Date.now(), ...dbUser, - ...fullUser, + ...user, _id, password: hashedPassword, tenantId, @@ -189,10 +188,36 @@ const validateUniqueUser = async (email: string, tenantId: string) => { } } +export async function isPreventSSOPasswords(user: User) { + // when in maintenance mode we allow sso users with the admin role + // to perform any password action - this prevents lockout + if (env.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) { + return false + } + + // Check local sso + if (isSSOUser(user)) { + return true + } + + // Check account sso + const account = await accountSdk.api.getAccount(user.email) + return !!(account && isSSOAccount(account)) +} + +export async function updateSelf(id: string, data: UpdateSelf) { + let user = await getUser(id) + user = { + ...user, + ...data, + } + return save(user) +} + export const save = async ( - user: User | ThirdPartyUser, + user: User, opts: SaveUserOpts = {} -): Promise => { +): Promise => { // default booleans to true if (opts.hashPassword == null) { opts.hashPassword = true @@ -264,7 +289,7 @@ export const save = async ( builtUser._rev = response.rev await eventHelpers.handleSaveEvents(builtUser, dbUser) - await addTenant(tenantId, _id, email) + await platform.users.addUser(tenantId, builtUser._id!, builtUser.email) await cache.user.invalidateUser(response.id) // let server know to sync user @@ -272,11 +297,8 @@ export const save = async ( await Promise.all(groupPromises) - return { - _id: response.id, - _rev: response.rev, - email, - } + // finally returned the saved user from the db + return db.get(builtUser._id!) } catch (err: any) { if (err.status === 409) { throw "User exists already" @@ -286,21 +308,6 @@ export const save = async ( } } -export const addTenant = async ( - tenantId: string, - _id: string, - email: string -) => { - if (env.MULTI_TENANCY) { - const afterCreateTenant = () => - migrations.backPopulateMigrations({ - type: MigrationType.GLOBAL, - tenantId, - }) - await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant) - } -} - const getExistingTenantUsers = async (emails: string[]): Promise => { const lcEmails = emails.map(email => email.toLowerCase()) const params = { @@ -432,7 +439,7 @@ export const bulkCreate = async ( for (const user of usersToBulkSave) { // TODO: Refactor to bulk insert users into the info db // instead of relying on looping tenant creation - await addTenant(tenantId, user._id, user.email) + await platform.users.addUser(tenantId, user._id, user.email) await eventHelpers.handleSaveEvents(user, undefined) await apps.syncUserInApps(user._id) } @@ -566,7 +573,7 @@ export const destroy = async (id: string, currentUser: any) => { } } - await deprovisioning.removeUserFromInfoDB(dbUser) + await platform.users.removeUser(dbUser) await db.remove(userId, dbUser._rev) @@ -579,7 +586,7 @@ export const destroy = async (id: string, currentUser: any) => { const bulkDeleteProcessing = async (dbUser: User) => { const userId = dbUser._id as string - await deprovisioning.removeUserFromInfoDB(dbUser) + await platform.users.removeUser(dbUser) await eventHelpers.handleDeleteEvents(dbUser) await cache.user.invalidateUser(userId) await sessions.invalidateSessions(userId, { reason: "bulk-deletion" }) diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 7d075e7fef..3004d0aed4 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -22,7 +22,7 @@ import { env as coreEnv, } from "@budibase/backend-core" import structures, { CSRF_TOKEN } from "./structures" -import { CreateUserResponse, User, AuthToken } from "@budibase/types" +import { SaveUserResponse, User, AuthToken } from "@budibase/types" import API from "./api" class TestConfiguration { @@ -226,7 +226,7 @@ class TestConfiguration { user = structures.users.user() } const response = await this._req(user, null, controllers.users.save) - const body = response as CreateUserResponse + const body = response as SaveUserResponse return this.getUser(body.email) } diff --git a/packages/worker/src/tests/api/auth.ts b/packages/worker/src/tests/api/auth.ts index ccbf747273..bd0471ca74 100644 --- a/packages/worker/src/tests/api/auth.ts +++ b/packages/worker/src/tests/api/auth.ts @@ -1,21 +1,39 @@ -import structures from "../structures" import TestConfiguration from "../TestConfiguration" -import { TestAPI } from "./base" +import { TestAPI, TestAPIOpts } from "./base" export class AuthAPI extends TestAPI { constructor(config: TestConfiguration) { super(config) } - updatePassword = (code: string) => { + updatePassword = ( + resetCode: string, + password: string, + opts?: TestAPIOpts + ) => { return this.request .post(`/api/global/auth/${this.config.getTenantId()}/reset/update`) .send({ - password: "newpassword", - resetCode: code, + password, + resetCode, }) .expect("Content-Type", /json/) - .expect(200) + .expect(opts?.status ? opts.status : 200) + } + + login = ( + tenantId: string, + email: string, + password: string, + opts?: TestAPIOpts + ) => { + return this.request + .post(`/api/global/auth/${tenantId}/login`) + .send({ + username: email, + password: password, + }) + .expect(opts?.status ? opts.status : 200) } logout = () => { @@ -25,25 +43,31 @@ export class AuthAPI extends TestAPI { .expect(200) } - requestPasswordReset = async (sendMailMock: any, userEmail: string) => { + requestPasswordReset = async ( + sendMailMock: any, + email: string, + opts?: TestAPIOpts + ) => { await this.config.saveSmtpConfig() await this.config.saveSettingsConfig() - await this.config.createUser({ - ...structures.users.user(), - email: userEmail, - }) + const res = await this.request .post(`/api/global/auth/${this.config.getTenantId()}/reset`) .send({ - email: userEmail, + email: email, }) .expect("Content-Type", /json/) - .expect(200) - const emailCall = sendMailMock.mock.calls[0][0] - const parts = emailCall.html.split( - `http://localhost:10000/builder/auth/reset?code=` - ) - const code = parts[1].split('"')[0].split("&")[0] + .expect(opts?.status ? opts.status : 200) + + let code: string | undefined + if (res.status === 200) { + const emailCall = sendMailMock.mock.calls[0][0] + const parts = emailCall.html.split( + `http://localhost:10000/builder/auth/reset?code=` + ) + code = parts[1].split('"')[0].split("&")[0] + } + return { code, res } } } diff --git a/packages/worker/src/tests/structures/index.ts b/packages/worker/src/tests/structures/index.ts index dad055f7a7..bec15df6dd 100644 --- a/packages/worker/src/tests/structures/index.ts +++ b/packages/worker/src/tests/structures/index.ts @@ -1,6 +1,5 @@ import { structures } from "@budibase/backend-core/tests" import * as configs from "./configs" -import * as users from "./users" import * as groups from "./groups" import { v4 as uuid } from "uuid" @@ -11,7 +10,6 @@ const pkg = { ...structures, uuid, configs, - users, TENANT_ID, CSRF_TOKEN, groups, diff --git a/packages/worker/src/tests/structures/users.ts b/packages/worker/src/tests/structures/users.ts deleted file mode 100644 index 3348670b7d..0000000000 --- a/packages/worker/src/tests/structures/users.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const email = "test@test.com" -import { AdminUser, BuilderUser, User } from "@budibase/types" -import { v4 as uuid } from "uuid" - -export const newEmail = () => { - return `${uuid()}@test.com` -} - -export const user = (userProps?: any): User => { - return { - email: newEmail(), - password: "test", - roles: { app_test: "admin" }, - ...userProps, - } -} - -export const adminUser = (userProps?: any): AdminUser => { - return { - ...user(userProps), - admin: { - global: true, - }, - builder: { - global: true, - }, - } -} - -export const builderUser = (userProps?: any): BuilderUser => { - return { - ...user(userProps), - builder: { - global: true, - }, - } -} diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index 7ec3447707..66e860edcb 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -26,7 +26,7 @@ type SendEmailOpts = { automation?: boolean } -const TEST_MODE = false +const TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev() const TYPE = TemplateType.EMAIL const FULL_EMAIL_PURPOSES = [ @@ -62,8 +62,8 @@ function createSMTPTransport(config: any) { host: "smtp.ethereal.email", secure: false, auth: { - user: "don.bahringer@ethereal.email", - pass: "yCKSH8rWyUPbnhGYk9", + user: "wyatt.zulauf29@ethereal.email", + pass: "tEwDtHBWWxusVWAPfa", }, } } From bd16f3a55dfe802cc11bc2c7ecac8434494cb0fc Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 21 Feb 2023 09:03:29 +0000 Subject: [PATCH 07/12] Re-add maxWorkers=2 to worker tests --- packages/worker/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/package.json b/packages/worker/package.json index 1ae0327441..0248199341 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -22,7 +22,7 @@ "build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION", "dev:stack:init": "node ./scripts/dev/manage.js init", "dev:builder": "npm run dev:stack:init && nodemon", - "test": "jest --coverage", + "test": "jest --coverage --maxWorkers=2", "test:watch": "jest --watch", "env:multi:enable": "node scripts/multiTenancy.js enable", "env:multi:disable": "node scripts/multiTenancy.js disable", From 0c838196e4751ffea9ace440ebb8d357f69393e8 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 21 Feb 2023 10:52:11 +0000 Subject: [PATCH 08/12] Enable mock redis for integration tests --- packages/builder/setup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/builder/setup.js b/packages/builder/setup.js index 744c20896a..b3fd96877b 100644 --- a/packages/builder/setup.js +++ b/packages/builder/setup.js @@ -19,6 +19,7 @@ process.env.COUCH_DB_USER = "budibase" process.env.COUCH_DB_PASSWORD = "budibase" process.env.INTERNAL_API_KEY = "budibase" process.env.ALLOW_DEV_AUTOMATIONS = 1 +process.env.MOCK_REDIS = 1 // Stop info logs polluting test outputs process.env.LOG_LEVEL = "error" From e6d7c22efa0a82c65020ef37602131508bf51b83 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 11:22:46 +0000 Subject: [PATCH 09/12] v2.3.17-alpha.5 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index dda0b8dfc8..63e9ac45f9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 75c16447ba..ceafebcbc5 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.17-alpha.4", + "@budibase/types": "2.3.17-alpha.5", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index bf2782d843..da9fdbca60 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.17-alpha.4", + "@budibase/string-templates": "2.3.17-alpha.5", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 049b8d2a9f..00fe30b057 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.4", - "@budibase/client": "2.3.17-alpha.4", - "@budibase/frontend-core": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", + "@budibase/bbui": "2.3.17-alpha.5", + "@budibase/client": "2.3.17-alpha.5", + "@budibase/frontend-core": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.5", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1d92be8c84..036120eac5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", - "@budibase/types": "2.3.17-alpha.4", + "@budibase/backend-core": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/types": "2.3.17-alpha.5", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index b40f27f287..97ebc2b2e0 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.4", - "@budibase/frontend-core": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", + "@budibase/bbui": "2.3.17-alpha.5", + "@budibase/frontend-core": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.5", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index c4222bd0e3..7a42ba09db 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.17-alpha.4", + "@budibase/bbui": "2.3.17-alpha.5", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index bd6fa2a9d4..36328c51d8 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 0388465ce4..3888c55f46 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.17-alpha.4", - "@budibase/client": "2.3.17-alpha.4", + "@budibase/backend-core": "2.3.17-alpha.5", + "@budibase/client": "2.3.17-alpha.5", "@budibase/pro": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", - "@budibase/types": "2.3.17-alpha.4", + "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/types": "2.3.17-alpha.5", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index fad3b4d59f..d5eb7e3f04 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index a5441b58a2..0b1ee30b2e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 0248199341..b6bc942add 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.4", + "@budibase/backend-core": "2.3.17-alpha.5", "@budibase/pro": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", - "@budibase/types": "2.3.17-alpha.4", + "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/types": "2.3.17-alpha.5", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From cbddc7ee4f537e78f01450eeb4c62b82e2882c9f Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 11:26:45 +0000 Subject: [PATCH 10/12] Update pro version to 2.3.17-alpha.5 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 3888c55f46..c647378d54 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.17-alpha.5", "@budibase/client": "2.3.17-alpha.5", - "@budibase/pro": "2.3.17-alpha.4", + "@budibase/pro": "2.3.17-alpha.5", "@budibase/string-templates": "2.3.17-alpha.5", "@budibase/types": "2.3.17-alpha.5", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 3fa6a84ad9..2d6ad91005 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.4.tgz#efe13cc2eb9a02223cff46228af8370a5a91db11" - integrity sha512-h9aCz+5uAQOvQiimULShKkXdQgyiwvgcxq3wqEvNHsAxmlslaJJouStWhvxBdsFiOW9K2CyI/8PeUBBRlAU0AQ== +"@budibase/backend-core@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.5.tgz#5bd380c9bf3835694729f1c68c44a77aec7b66dd" + integrity sha512-Nv/BsmItQcdi30oHvNEeRRkDxooMR3shZksb14NDWfdP9gxjVOkUltA2HELujL2ZEg4mismau/0oy9kZy5z3Og== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.4" + "@budibase/types" "2.3.17-alpha.5" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.4.tgz#56e945bd960eca8c8b2a04a3dd606df392bf5792" - integrity sha512-DJtKCc5/XXAnrvI5sS+joVuDYOLgwFmDYwLbssJgSQzGXNqepN/ikGq6eFCKn/99fRYBGREfwXwAlxXveZxPWA== +"@budibase/pro@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.5.tgz#4007ab38a5a88cedc1c0ba48543fcbfdf0c58a2a" + integrity sha512-pR7i7ehpoJ08Pq4Xs5vFJcZLvLkfCDS3AvJV1e0j9IDPD73kqiqCxgOF9rE27NTLf0BvbPK5Vj4Z7RPCxIJv7g== dependencies: - "@budibase/backend-core" "2.3.17-alpha.4" - "@budibase/types" "2.3.17-alpha.4" + "@budibase/backend-core" "2.3.17-alpha.5" + "@budibase/types" "2.3.17-alpha.5" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.4.tgz#63eb756160da7ace5459e303874f5e67425c6988" - integrity sha512-us/gGZPHimHsYNAnJ5yGdxeThT065wofJ2sfg0aD81P8nq3F2mevBoUmci/KRYdrQ9EsgpZ3Ou6CqnhnV8WmfA== +"@budibase/types@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.5.tgz#a8c31f49ac9f8b312317fd6fe547ea76d89d059b" + integrity sha512-qp51pVnOaUf42plQjPlCXZVMTIFKrkkhiJ+XaClnekRmyAYaz7JUIex+QTDbmUF4i0WbZRc0MgUCVQxlKkWotA== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index b6bc942add..c1a1a8a001 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.17-alpha.5", - "@budibase/pro": "2.3.17-alpha.4", + "@budibase/pro": "2.3.17-alpha.5", "@budibase/string-templates": "2.3.17-alpha.5", "@budibase/types": "2.3.17-alpha.5", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 3c12fd53b6..5c534eefe1 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.4.tgz#efe13cc2eb9a02223cff46228af8370a5a91db11" - integrity sha512-h9aCz+5uAQOvQiimULShKkXdQgyiwvgcxq3wqEvNHsAxmlslaJJouStWhvxBdsFiOW9K2CyI/8PeUBBRlAU0AQ== +"@budibase/backend-core@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.5.tgz#5bd380c9bf3835694729f1c68c44a77aec7b66dd" + integrity sha512-Nv/BsmItQcdi30oHvNEeRRkDxooMR3shZksb14NDWfdP9gxjVOkUltA2HELujL2ZEg4mismau/0oy9kZy5z3Og== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.4" + "@budibase/types" "2.3.17-alpha.5" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.4.tgz#56e945bd960eca8c8b2a04a3dd606df392bf5792" - integrity sha512-DJtKCc5/XXAnrvI5sS+joVuDYOLgwFmDYwLbssJgSQzGXNqepN/ikGq6eFCKn/99fRYBGREfwXwAlxXveZxPWA== +"@budibase/pro@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.5.tgz#4007ab38a5a88cedc1c0ba48543fcbfdf0c58a2a" + integrity sha512-pR7i7ehpoJ08Pq4Xs5vFJcZLvLkfCDS3AvJV1e0j9IDPD73kqiqCxgOF9rE27NTLf0BvbPK5Vj4Z7RPCxIJv7g== dependencies: - "@budibase/backend-core" "2.3.17-alpha.4" - "@budibase/types" "2.3.17-alpha.4" + "@budibase/backend-core" "2.3.17-alpha.5" + "@budibase/types" "2.3.17-alpha.5" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.4.tgz#63eb756160da7ace5459e303874f5e67425c6988" - integrity sha512-us/gGZPHimHsYNAnJ5yGdxeThT065wofJ2sfg0aD81P8nq3F2mevBoUmci/KRYdrQ9EsgpZ3Ou6CqnhnV8WmfA== +"@budibase/types@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.5.tgz#a8c31f49ac9f8b312317fd6fe547ea76d89d059b" + integrity sha512-qp51pVnOaUf42plQjPlCXZVMTIFKrkkhiJ+XaClnekRmyAYaz7JUIex+QTDbmUF4i0WbZRc0MgUCVQxlKkWotA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From 9ed759580f436e598070dc44fbdd3b520491e4f3 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 11:52:45 +0000 Subject: [PATCH 11/12] v2.3.17-alpha.6 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 63e9ac45f9..80460af875 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index ceafebcbc5..68323e05ac 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.17-alpha.5", + "@budibase/types": "2.3.17-alpha.6", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index da9fdbca60..ccaadf77b1 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.6", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 00fe30b057..761f17dd6d 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.5", - "@budibase/client": "2.3.17-alpha.5", - "@budibase/frontend-core": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/bbui": "2.3.17-alpha.6", + "@budibase/client": "2.3.17-alpha.6", + "@budibase/frontend-core": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.6", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 036120eac5..a4c21bef29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", - "@budibase/types": "2.3.17-alpha.5", + "@budibase/backend-core": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/types": "2.3.17-alpha.6", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index 97ebc2b2e0..8b52e19c87 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.5", - "@budibase/frontend-core": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/bbui": "2.3.17-alpha.6", + "@budibase/frontend-core": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.6", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 7a42ba09db..0ef09e42e2 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.17-alpha.5", + "@budibase/bbui": "2.3.17-alpha.6", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 36328c51d8..fc95ba8106 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index c647378d54..5c887c1a5b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.17-alpha.5", - "@budibase/client": "2.3.17-alpha.5", + "@budibase/backend-core": "2.3.17-alpha.6", + "@budibase/client": "2.3.17-alpha.6", "@budibase/pro": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", - "@budibase/types": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/types": "2.3.17-alpha.6", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index d5eb7e3f04..b53eaf887e 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 0b1ee30b2e..c913fea299 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index c1a1a8a001..a7b280f203 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.5", + "@budibase/backend-core": "2.3.17-alpha.6", "@budibase/pro": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", - "@budibase/types": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/types": "2.3.17-alpha.6", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From e093ab5ef4992058e4a74181638793ac4b6edd98 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 11:56:26 +0000 Subject: [PATCH 12/12] Update pro version to 2.3.17-alpha.6 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 5c887c1a5b..1d852473f2 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.17-alpha.6", "@budibase/client": "2.3.17-alpha.6", - "@budibase/pro": "2.3.17-alpha.5", + "@budibase/pro": "2.3.17-alpha.6", "@budibase/string-templates": "2.3.17-alpha.6", "@budibase/types": "2.3.17-alpha.6", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 2d6ad91005..72f2bc4db2 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.5.tgz#5bd380c9bf3835694729f1c68c44a77aec7b66dd" - integrity sha512-Nv/BsmItQcdi30oHvNEeRRkDxooMR3shZksb14NDWfdP9gxjVOkUltA2HELujL2ZEg4mismau/0oy9kZy5z3Og== +"@budibase/backend-core@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.6.tgz#b083b3899d435694105d37a1ec817ec6cdc9bb07" + integrity sha512-8ljXZnK6Db3Mexk+MyIsrQBFFu6aV3eRtiw5pwHl9BobxR+6s79YmUgLfjZCfpgzMXqnT73FnV7g8K7KPTNC3g== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.5" + "@budibase/types" "2.3.17-alpha.6" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.5.tgz#4007ab38a5a88cedc1c0ba48543fcbfdf0c58a2a" - integrity sha512-pR7i7ehpoJ08Pq4Xs5vFJcZLvLkfCDS3AvJV1e0j9IDPD73kqiqCxgOF9rE27NTLf0BvbPK5Vj4Z7RPCxIJv7g== +"@budibase/pro@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.6.tgz#2d24afa48003ff5928f667efeff16735bed24e0c" + integrity sha512-yVxKCHiDE4yoARDLZ3IqktUuWgNZoEVlmWo9rS8+bxPcsf5sw7Rj7TXTCMQX7F0cekMVtG+KkpH/8bFrqfR6OA== dependencies: - "@budibase/backend-core" "2.3.17-alpha.5" - "@budibase/types" "2.3.17-alpha.5" + "@budibase/backend-core" "2.3.17-alpha.6" + "@budibase/types" "2.3.17-alpha.6" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.5.tgz#a8c31f49ac9f8b312317fd6fe547ea76d89d059b" - integrity sha512-qp51pVnOaUf42plQjPlCXZVMTIFKrkkhiJ+XaClnekRmyAYaz7JUIex+QTDbmUF4i0WbZRc0MgUCVQxlKkWotA== +"@budibase/types@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.6.tgz#85b148334312a41cfbf01b6a20417b5e20485f3f" + integrity sha512-BgPvLdNQKJSnJmHNo1OfKSHEeVhdTwcNSr2cwHjUpJk394rJiZfsOV7it8M9dLtAtpdsNR3ns7L6biW+pfjYoQ== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index a7b280f203..d595623fa4 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.17-alpha.6", - "@budibase/pro": "2.3.17-alpha.5", + "@budibase/pro": "2.3.17-alpha.6", "@budibase/string-templates": "2.3.17-alpha.6", "@budibase/types": "2.3.17-alpha.6", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 5c534eefe1..3273b78d68 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.5.tgz#5bd380c9bf3835694729f1c68c44a77aec7b66dd" - integrity sha512-Nv/BsmItQcdi30oHvNEeRRkDxooMR3shZksb14NDWfdP9gxjVOkUltA2HELujL2ZEg4mismau/0oy9kZy5z3Og== +"@budibase/backend-core@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.6.tgz#b083b3899d435694105d37a1ec817ec6cdc9bb07" + integrity sha512-8ljXZnK6Db3Mexk+MyIsrQBFFu6aV3eRtiw5pwHl9BobxR+6s79YmUgLfjZCfpgzMXqnT73FnV7g8K7KPTNC3g== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.5" + "@budibase/types" "2.3.17-alpha.6" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.5.tgz#4007ab38a5a88cedc1c0ba48543fcbfdf0c58a2a" - integrity sha512-pR7i7ehpoJ08Pq4Xs5vFJcZLvLkfCDS3AvJV1e0j9IDPD73kqiqCxgOF9rE27NTLf0BvbPK5Vj4Z7RPCxIJv7g== +"@budibase/pro@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.6.tgz#2d24afa48003ff5928f667efeff16735bed24e0c" + integrity sha512-yVxKCHiDE4yoARDLZ3IqktUuWgNZoEVlmWo9rS8+bxPcsf5sw7Rj7TXTCMQX7F0cekMVtG+KkpH/8bFrqfR6OA== dependencies: - "@budibase/backend-core" "2.3.17-alpha.5" - "@budibase/types" "2.3.17-alpha.5" + "@budibase/backend-core" "2.3.17-alpha.6" + "@budibase/types" "2.3.17-alpha.6" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.5.tgz#a8c31f49ac9f8b312317fd6fe547ea76d89d059b" - integrity sha512-qp51pVnOaUf42plQjPlCXZVMTIFKrkkhiJ+XaClnekRmyAYaz7JUIex+QTDbmUF4i0WbZRc0MgUCVQxlKkWotA== +"@budibase/types@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.6.tgz#85b148334312a41cfbf01b6a20417b5e20485f3f" + integrity sha512-BgPvLdNQKJSnJmHNo1OfKSHEeVhdTwcNSr2cwHjUpJk394rJiZfsOV7it8M9dLtAtpdsNR3ns7L6biW+pfjYoQ== "@cspotcode/source-map-support@^0.8.0": version "0.8.1"