diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index e0263546ff..f196b70db4 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -10,7 +10,7 @@ on: pull_request: branches: - master - - develop + - develop workflow_dispatch: env: diff --git a/lerna.json b/lerna.json index 5145222dfa..0e1e6a8711 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 428d785a44..1243f58abe 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "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.18-alpha.12", + "@budibase/types": "2.3.18-alpha.13", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index f34d54a879..936d06ddff 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -2,25 +2,34 @@ const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy import { getGlobalDB } from "../context" -const refresh = require("passport-oauth2-refresh") -import { Config, Cookie } from "../constants" -import { getScopedConfig } from "../db" +import { Cookie } from "../constants" import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { + authenticated, + csrf, + google, jwt as jwtPassport, local, - authenticated, - tenancy, - csrf, oidc, - google, + tenancy, } from "../middleware" +import * as userCache from "../cache/user" import { invalidateUser } from "../cache/user" -import { PlatformLogoutOpts, User } from "@budibase/types" +import { + ConfigType, + GoogleInnerConfig, + OIDCInnerConfig, + PlatformLogoutOpts, + SSOProviderType, + User, +} from "@budibase/types" import { logAlert } from "../logging" import * as events from "../events" -import * as userCache from "../cache/user" +import * as configs from "../configs" import { clearCookie, getCookie } from "../utils" +import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" + +const refresh = require("passport-oauth2-refresh") export { auditLog, authError, @@ -33,7 +42,6 @@ export { google, oidc, } from "../middleware" -import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" export const buildAuthMiddleware = authenticated export const buildTenancyMiddleware = tenancy export const buildCsrfMiddleware = csrf @@ -63,11 +71,10 @@ _passport.deserializeUser(async (user: User, done: any) => { }) async function refreshOIDCAccessToken( - db: any, - chosenConfig: any, + chosenConfig: OIDCInnerConfig, refreshToken: string -) { - const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) +): Promise { + const callbackUrl = await oidc.getCallbackUrl() let enrichedConfig: any let strategy: any @@ -90,7 +97,7 @@ async function refreshOIDCAccessToken( return new Promise(resolve => { refresh.requestNewAccessToken( - Config.OIDC, + ConfigType.OIDC, refreshToken, (err: any, accessToken: string, refreshToken: any, params: any) => { resolve({ err, accessToken, refreshToken, params }) @@ -100,11 +107,10 @@ async function refreshOIDCAccessToken( } async function refreshGoogleAccessToken( - db: any, - config: any, + config: GoogleInnerConfig, refreshToken: any -) { - let callbackUrl = await google.getCallbackUrl(db, config) +): Promise { + let callbackUrl = await google.getCallbackUrl(config) let strategy try { @@ -124,7 +130,7 @@ async function refreshGoogleAccessToken( return new Promise(resolve => { refresh.requestNewAccessToken( - Config.GOOGLE, + ConfigType.GOOGLE, refreshToken, (err: any, accessToken: string, refreshToken: string, params: any) => { resolve({ err, accessToken, refreshToken, params }) @@ -133,41 +139,37 @@ async function refreshGoogleAccessToken( }) } +interface RefreshResponse { + err?: { + data?: string + } + accessToken?: string + refreshToken?: string + params?: any +} + export async function refreshOAuthToken( refreshToken: string, - configType: string, - configId: string -) { - const db = getGlobalDB() - - const config = await getScopedConfig(db, { - type: configType, - group: {}, - }) - - let chosenConfig = {} - let refreshResponse - if (configType === Config.OIDC) { - // configId - retrieved from cookie. - chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] - if (!chosenConfig) { - throw new Error("Invalid OIDC configuration") - } - refreshResponse = await refreshOIDCAccessToken( - db, - chosenConfig, - refreshToken - ) - } else { - chosenConfig = config - refreshResponse = await refreshGoogleAccessToken( - db, - chosenConfig, - refreshToken - ) + providerType: SSOProviderType, + configId?: string +): Promise { + switch (providerType) { + case SSOProviderType.OIDC: + if (!configId) { + return { err: { data: "OIDC config id not provided" } } + } + const oidcConfig = await configs.getOIDCConfigById(configId) + if (!oidcConfig) { + return { err: { data: "OIDC configuration not found" } } + } + return refreshOIDCAccessToken(oidcConfig, refreshToken) + case SSOProviderType.GOOGLE: + let googleConfig = await configs.getGoogleConfig() + if (!googleConfig) { + return { err: { data: "Google configuration not found" } } + } + return refreshGoogleAccessToken(googleConfig, refreshToken) } - - return refreshResponse } // TODO: Refactor to use user save function instead to prevent the need for diff --git a/packages/backend-core/src/configs/configs.ts b/packages/backend-core/src/configs/configs.ts new file mode 100644 index 0000000000..882c37ceb9 --- /dev/null +++ b/packages/backend-core/src/configs/configs.ts @@ -0,0 +1,224 @@ +import { + Config, + ConfigType, + GoogleConfig, + GoogleInnerConfig, + OIDCConfig, + OIDCInnerConfig, + SettingsConfig, + SettingsInnerConfig, + SMTPConfig, + SMTPInnerConfig, +} from "@budibase/types" +import { DocumentType, SEPARATOR } from "../constants" +import { CacheKey, TTL, withCache } from "../cache" +import * as context from "../context" +import env from "../environment" +import environment from "../environment" + +// UTILS + +/** + * Generates a new configuration ID. + * @returns {string} The new configuration ID which the config doc can be stored under. + */ +export function generateConfigID(type: ConfigType) { + return `${DocumentType.CONFIG}${SEPARATOR}${type}` +} + +export async function getConfig( + type: ConfigType +): Promise { + const db = context.getGlobalDB() + try { + // await to catch error + const config = (await db.get(generateConfigID(type))) as T + return config + } catch (e: any) { + if (e.status === 404) { + return + } + throw e + } +} + +export async function save(config: Config) { + const db = context.getGlobalDB() + return db.put(config) +} + +// SETTINGS + +export async function getSettingsConfigDoc(): Promise { + let config = await getConfig(ConfigType.SETTINGS) + + if (!config) { + config = { + _id: generateConfigID(ConfigType.GOOGLE), + type: ConfigType.SETTINGS, + config: {}, + } + } + + // overridden fields + config.config.platformUrl = await getPlatformUrl({ + tenantAware: true, + config: config.config, + }) + config.config.analyticsEnabled = await analyticsEnabled({ + config: config.config, + }) + + return config +} + +export async function getSettingsConfig(): Promise { + return (await getSettingsConfigDoc()).config +} + +export async function getPlatformUrl( + opts: { tenantAware: boolean; config?: SettingsInnerConfig } = { + tenantAware: true, + } +) { + let platformUrl = env.PLATFORM_URL || "http://localhost:10000" + + if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) { + // cloud and multi tenant - add the tenant to the default platform url + const tenantId = context.getTenantId() + if (!platformUrl.includes("localhost:")) { + platformUrl = platformUrl.replace("://", `://${tenantId}.`) + } + } else if (env.SELF_HOSTED) { + const config = opts?.config + ? opts.config + : // direct to db to prevent infinite loop + (await getConfig(ConfigType.SETTINGS))?.config + if (config?.platformUrl) { + platformUrl = config.platformUrl + } + } + + return platformUrl +} + +export const analyticsEnabled = async (opts?: { + config?: SettingsInnerConfig +}) => { + // cloud - always use the environment variable + if (!env.SELF_HOSTED) { + return !!env.ENABLE_ANALYTICS + } + + // self host - prefer the settings doc + // use cache as events have high throughput + const enabledInDB = await withCache( + CacheKey.ANALYTICS_ENABLED, + TTL.ONE_DAY, + async () => { + const config = opts?.config + ? opts.config + : // direct to db to prevent infinite loop + (await getConfig(ConfigType.SETTINGS))?.config + + // need to do explicit checks in case the field is not set + if (config?.analyticsEnabled === false) { + return false + } else if (config?.analyticsEnabled === true) { + return true + } + } + ) + + if (enabledInDB !== undefined) { + return enabledInDB + } + + // fallback to the environment variable + // explicitly check for 0 or false here, undefined or otherwise is treated as true + const envEnabled: any = env.ENABLE_ANALYTICS + if (envEnabled === 0 || envEnabled === false) { + return false + } else { + return true + } +} + +// GOOGLE + +async function getGoogleConfigDoc(): Promise { + return await getConfig(ConfigType.GOOGLE) +} + +export async function getGoogleConfig(): Promise< + GoogleInnerConfig | undefined +> { + const config = await getGoogleConfigDoc() + if (config) { + return config.config + } + + // Use google fallback configuration from env variables + if (environment.GOOGLE_CLIENT_ID && environment.GOOGLE_CLIENT_SECRET) { + return { + clientID: environment.GOOGLE_CLIENT_ID!, + clientSecret: environment.GOOGLE_CLIENT_SECRET!, + activated: true, + } + } +} + +// OIDC + +async function getOIDCConfigDoc(): Promise { + return getConfig(ConfigType.OIDC) +} + +export async function getOIDCConfig(): Promise { + const config = (await getOIDCConfigDoc())?.config + // default to the 0th config + return config?.configs && config.configs[0] +} + +/** + * @param configId The config id of the inner config to retrieve + */ +export async function getOIDCConfigById( + configId: string +): Promise { + const config = (await getConfig(ConfigType.OIDC))?.config + return config && config.configs.filter((c: any) => c.uuid === configId)[0] +} + +// SMTP + +export async function getSMTPConfigDoc(): Promise { + return getConfig(ConfigType.SMTP) +} + +export async function getSMTPConfig( + isAutomation?: boolean +): Promise { + const config = await getSMTPConfigDoc() + if (config) { + return config.config + } + + // always allow fallback in self host + // in cloud don't allow for automations + const allowFallback = env.SELF_HOSTED || !isAutomation + + // Use an SMTP fallback configuration from env variables + if (env.SMTP_FALLBACK_ENABLED && allowFallback) { + return { + port: env.SMTP_PORT, + host: env.SMTP_HOST!, + secure: false, + from: env.SMTP_FROM_ADDRESS!, + auth: { + user: env.SMTP_USER!, + pass: env.SMTP_PASSWORD!, + }, + } + } +} diff --git a/packages/backend-core/src/configs/index.ts b/packages/backend-core/src/configs/index.ts new file mode 100644 index 0000000000..783f22a0b9 --- /dev/null +++ b/packages/backend-core/src/configs/index.ts @@ -0,0 +1 @@ +export * from "./configs" diff --git a/packages/backend-core/src/configs/tests/configs.spec.ts b/packages/backend-core/src/configs/tests/configs.spec.ts new file mode 100644 index 0000000000..079f2ab681 --- /dev/null +++ b/packages/backend-core/src/configs/tests/configs.spec.ts @@ -0,0 +1,116 @@ +import { DBTestConfiguration, generator, testEnv } from "../../../tests" +import { ConfigType } from "@budibase/types" +import env from "../../environment" +import * as configs from "../configs" + +const DEFAULT_URL = "http://localhost:10000" +const ENV_URL = "http://env.com" + +describe("configs", () => { + const config = new DBTestConfiguration() + + const setDbPlatformUrl = async (dbUrl: string) => { + const settingsConfig = { + _id: configs.generateConfigID(ConfigType.SETTINGS), + type: ConfigType.SETTINGS, + config: { + platformUrl: dbUrl, + }, + } + await configs.save(settingsConfig) + } + + beforeEach(async () => { + config.newTenant() + }) + + describe("getPlatformUrl", () => { + describe("self host", () => { + beforeEach(async () => { + testEnv.selfHosted() + }) + + it("gets the default url", async () => { + await config.doInTenant(async () => { + const url = await configs.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 configs.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 configs.getPlatformUrl() + expect(url).toBe(dbUrl) + }) + }) + }) + + describe("cloud", () => { + function getTenantAwareUrl() { + return `http://${config.tenantId}.env.com` + } + + beforeEach(async () => { + testEnv.cloudHosted() + testEnv.multiTenant() + + env._set("PLATFORM_URL", ENV_URL) + }) + + it("gets the platform url from the environment without tenancy", async () => { + await config.doInTenant(async () => { + const url = await configs.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 configs.getPlatformUrl() + expect(url).toBe(getTenantAwareUrl()) + }) + }) + + it("never gets the platform url from the database", async () => { + await config.doInTenant(async () => { + await setDbPlatformUrl(generator.url()) + const url = await configs.getPlatformUrl() + expect(url).toBe(getTenantAwareUrl()) + }) + }) + }) + }) + + describe("getSettingsConfig", () => { + beforeAll(async () => { + testEnv.selfHosted() + env._set("PLATFORM_URL", "") + }) + + it("returns the platform url with an existing config", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const config = await configs.getSettingsConfig() + expect(config.platformUrl).toBe(dbUrl) + }) + }) + + it("returns the platform url without an existing config", async () => { + await config.doInTenant(async () => { + const config = await configs.getSettingsConfig() + 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 index 7bdca5ae8b..138457c65e 100644 --- a/packages/backend-core/src/db/tests/utils.spec.ts +++ b/packages/backend-core/src/db/tests/utils.spec.ts @@ -1,19 +1,13 @@ -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" +import { generateAppID } from "../utils" describe("utils", () => { - const config = new DBTestConfiguration() - - describe("app ID manipulation", () => { + describe("generateAppID", () => { function getID() { const appId = generateAppID() const split = appId.split("_") @@ -66,127 +60,4 @@ describe("utils", () => { 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/utils.ts b/packages/backend-core/src/db/utils.ts index 8ad7714973..76c52d08ad 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -9,12 +9,11 @@ import { InternalTable, APP_PREFIX, } from "../constants" -import { getTenantId, getGlobalDB, getGlobalDBName } from "../context" +import { getTenantId, getGlobalDBName } from "../context" import { doWithDB, directCouchAllDbs } from "./db" import { getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "./conversions" -import * as events from "../events" -import { App, Database, ConfigType, isSettingsConfig } from "@budibase/types" +import { App, Database } from "@budibase/types" /** * Generates a new app ID. @@ -412,32 +411,6 @@ export async function dbExists(dbName: any) { ) } -/** - * Generates a new configuration ID. - * @returns {string} The new configuration ID which the config doc can be stored under. - */ -export const generateConfigID = ({ type, workspace, user }: any) => { - const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) - - return `${DocumentType.CONFIG}${SEPARATOR}${scope}` -} - -/** - * Gets parameters for retrieving configurations. - */ -export const getConfigParams = ( - { type, workspace, user }: any, - otherProps = {} -) => { - const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) - - return { - ...otherProps, - startkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}`, - endkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`, - } -} - /** * Generates a new dev info document ID - this is scoped to a user. * @returns {string} The new dev info ID which info for dev (like api key) can be stored under. @@ -461,109 +434,6 @@ export const getPluginParams = (pluginId?: string | null, otherProps = {}) => { return getDocParams(DocumentType.PLUGIN, pluginId, otherProps) } -/** - * Returns the most granular configuration document from the DB based on the type, workspace and userID passed. - * @param {Object} db - db instance to query - * @param {Object} scopes - the type, workspace and userID scopes of the configuration. - * @returns The most granular configuration document based on the scope. - */ -export const getScopedFullConfig = async function ( - db: any, - { type, user, workspace }: any -) { - const response = await db.allDocs( - getConfigParams( - { type, user, workspace }, - { - include_docs: true, - } - ) - ) - - function determineScore(row: any) { - const config = row.doc - - // Config is specific to a user and a workspace - if (config._id.includes(generateConfigID({ type, user, workspace }))) { - return 4 - } else if (config._id.includes(generateConfigID({ type, user }))) { - // Config is specific to a user only - return 3 - } else if (config._id.includes(generateConfigID({ type, workspace }))) { - // Config is specific to a workspace only - return 2 - } else if (config._id.includes(generateConfigID({ type }))) { - // Config is specific to a type only - return 1 - } - return 0 - } - - // Find the config with the most granular scope based on context - let scopedConfig = response.rows.sort( - (a: any, b: any) => determineScore(a) - determineScore(b) - )[0] - - // custom logic for settings doc - if (type === ConfigType.SETTINGS) { - if (!scopedConfig || !scopedConfig.doc) { - // defaults - scopedConfig = { - doc: { - _id: generateConfigID({ type, user, workspace }), - type: ConfigType.SETTINGS, - config: { - platformUrl: await getPlatformUrl({ tenantAware: true }), - analyticsEnabled: await events.analytics.enabled(), - }, - }, - } - } - - // will always be true - use assertion function to get type access - if (isSettingsConfig(scopedConfig.doc)) { - // overrides affected by environment - scopedConfig.doc.config.platformUrl = await getPlatformUrl({ - tenantAware: true, - }) - scopedConfig.doc.config.analyticsEnabled = - await events.analytics.enabled() - } - } - - return scopedConfig && scopedConfig.doc -} - -export const getPlatformUrl = async (opts = { tenantAware: true }) => { - let platformUrl = env.PLATFORM_URL || "http://localhost:10000" - - if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) { - // cloud and multi tenant - add the tenant to the default platform url - const tenantId = getTenantId() - if (!platformUrl.includes("localhost:")) { - platformUrl = platformUrl.replace("://", `://${tenantId}.`) - } - } else if (env.SELF_HOSTED) { - const db = getGlobalDB() - // get the doc directly instead of with getScopedConfig to prevent loop - let settings - try { - settings = await db.get(generateConfigID({ type: ConfigType.SETTINGS })) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - - // self hosted - check for platform url override - if (settings && settings.config && settings.config.platformUrl) { - platformUrl = settings.config.platformUrl - } - } - - return platformUrl -} - export function pagination( data: any[], pageSize: number, @@ -597,8 +467,3 @@ export function pagination( nextPage, } } - -export async function getScopedConfig(db: any, params: any) { - const configDoc = await getScopedFullConfig(db, params) - return configDoc && configDoc.config ? configDoc.config : configDoc -} diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 6bcc59bcea..cd7bcca11d 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -28,6 +28,8 @@ const DefaultBucketName = { PLUGINS: "plugins", } +const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") + const environment = { isTest, isJest, @@ -58,7 +60,7 @@ const environment = { process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "", DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, - SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), + SELF_HOSTED: selfHosted, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, PLATFORM_URL: process.env.PLATFORM_URL || "", POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, @@ -85,6 +87,21 @@ const environment = { process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true, ENABLE_AUDIT_LOG_IP_ADDR: process.env.ENABLE_AUDIT_LOG_IP_ADDR, + // smtp + SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASSWORD: process.env.SMTP_PASSWORD, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: parseInt(process.env.SMTP_PORT || ""), + SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS, + /** + * 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/backend-core/src/events/analytics.ts b/packages/backend-core/src/events/analytics.ts index 7fbc6d9c2b..dcfd6d5104 100644 --- a/packages/backend-core/src/events/analytics.ts +++ b/packages/backend-core/src/events/analytics.ts @@ -1,55 +1,6 @@ -import env from "../environment" -import * as context from "../context" -import * as dbUtils from "../db/utils" -import { Config } from "../constants" -import { withCache, TTL, CacheKey } from "../cache" +import * as configs from "../configs" +// wrapper utility function export const enabled = async () => { - // cloud - always use the environment variable - if (!env.SELF_HOSTED) { - return !!env.ENABLE_ANALYTICS - } - - // self host - prefer the settings doc - // use cache as events have high throughput - const enabledInDB = await withCache( - CacheKey.ANALYTICS_ENABLED, - TTL.ONE_DAY, - async () => { - const settings = await getSettingsDoc() - - // need to do explicit checks in case the field is not set - if (settings?.config?.analyticsEnabled === false) { - return false - } else if (settings?.config?.analyticsEnabled === true) { - return true - } - } - ) - - if (enabledInDB !== undefined) { - return enabledInDB - } - - // fallback to the environment variable - // explicitly check for 0 or false here, undefined or otherwise is treated as true - const envEnabled: any = env.ENABLE_ANALYTICS - if (envEnabled === 0 || envEnabled === false) { - return false - } else { - return true - } -} - -const getSettingsDoc = async () => { - const db = context.getGlobalDB() - let settings - try { - settings = await db.get(dbUtils.generateConfigID({ type: Config.SETTINGS })) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - return settings + return configs.analyticsEnabled() } diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index dcb2ebb4ab..9534fb293d 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -10,7 +10,6 @@ import { isCloudAccount, isSSOAccount, TenantGroup, - SettingsConfig, CloudAccount, UserIdentity, InstallationGroup, @@ -19,10 +18,9 @@ import { isSSOUser, } from "@budibase/types" import { processors } from "./processors" -import * as dbUtils from "../db/utils" -import { Config } from "../constants" import { newid } from "../utils" import * as installation from "../installation" +import * as configs from "../configs" import { withCache, TTL, CacheKey } from "../cache/generic" const pkg = require("../../package.json") @@ -271,9 +269,7 @@ const getUniqueTenantId = async (tenantId: string): Promise => { return context.doInTenant(tenantId, () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { const db = context.getGlobalDB() - const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, { - type: Config.SETTINGS, - }) + const config = await configs.getSettingsConfigDoc() let uniqueTenantId: string if (config.config.uniqueTenantId) { diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 1cffcc3f7b..48569548e3 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -1,3 +1,4 @@ +export * as configs from "./configs" export * as events from "./events" export * as migrations from "./migrations" export * as users from "./users" diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index cce27823c0..d7e6346b3f 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -115,7 +115,8 @@ export default function ( authenticated = true } catch (err: any) { authenticated = false - console.error("Auth Error", err?.message || err) + console.error(`Auth Error: ${err.message}`) + console.error(err) // remove the cookie as the user does not exist anymore clearCookie(ctx, Cookie.Auth) } diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts index 5ac70c33e5..36aff2cdbc 100644 --- a/packages/backend-core/src/middleware/errorHandling.ts +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -11,6 +11,7 @@ export async function errorHandling(ctx: any, next: any) { if (status > 499 || env.ENABLE_4XX_HTTP_LOGGING) { ctx.log.error(err) + console.trace(err) } const error = errors.getPublicError(err) diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index 112f8d2096..ff5224e437 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -1,9 +1,8 @@ import * as google from "../sso/google" -import { Cookie, Config } from "../../../constants" +import { Cookie } from "../../../constants" import { clearCookie, getCookie } from "../../../utils" -import { getScopedConfig, getPlatformUrl, doWithDB } from "../../../db" -import environment from "../../../environment" -import { getGlobalDB } from "../../../context" +import { doWithDB } from "../../../db" +import * as configs from "../../../configs" import { BBContext, Database, SSOProfile } from "@budibase/types" import { ssoSaveUserNoOp } from "../sso/sso" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy @@ -13,18 +12,11 @@ type Passport = { } async function fetchGoogleCreds() { - // try and get the config from the tenant - const db = getGlobalDB() - const googleConfig = await getScopedConfig(db, { - type: Config.GOOGLE, - }) - // or fall back to env variables - return ( - googleConfig || { - clientID: environment.GOOGLE_CLIENT_ID, - clientSecret: environment.GOOGLE_CLIENT_SECRET, - } - ) + const config = await configs.getGoogleConfig() + if (!config) { + throw new Error("No google configuration found") + } + return config } export async function preAuth( @@ -34,7 +26,7 @@ export async function preAuth( ) { // get the relevant config const googleConfig = await fetchGoogleCreds() - const platformUrl = await getPlatformUrl({ tenantAware: false }) + const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` const strategy = await google.strategyFactory( @@ -61,7 +53,7 @@ export async function postAuth( ) { // get the relevant config const config = await fetchGoogleCreds() - const platformUrl = await getPlatformUrl({ tenantAware: false }) + const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth) diff --git a/packages/backend-core/src/middleware/passport/sso/google.ts b/packages/backend-core/src/middleware/passport/sso/google.ts index d26d7d6a8d..ad7593e63d 100644 --- a/packages/backend-core/src/middleware/passport/sso/google.ts +++ b/packages/backend-core/src/middleware/passport/sso/google.ts @@ -2,12 +2,11 @@ import { ssoCallbackUrl } from "../utils" import * as sso from "./sso" import { ConfigType, - GoogleConfig, - Database, SSOProfile, SSOAuthDetails, SSOProviderType, SaveSSOUserFunction, + GoogleInnerConfig, } from "@budibase/types" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy @@ -45,7 +44,7 @@ export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { * @returns Dynamically configured Passport Google Strategy */ export async function strategyFactory( - config: GoogleConfig["config"], + config: GoogleInnerConfig, callbackUrl: string, saveUserFn: SaveSSOUserFunction ) { @@ -73,9 +72,6 @@ export async function strategyFactory( } } -export async function getCallbackUrl( - db: Database, - config: { callbackURL?: string } -) { - return ssoCallbackUrl(db, config, ConfigType.GOOGLE) +export async function getCallbackUrl(config: GoogleInnerConfig) { + return ssoCallbackUrl(ConfigType.GOOGLE, config) } diff --git a/packages/backend-core/src/middleware/passport/sso/oidc.ts b/packages/backend-core/src/middleware/passport/sso/oidc.ts index 1fb44b84a3..b6d5eb52e9 100644 --- a/packages/backend-core/src/middleware/passport/sso/oidc.ts +++ b/packages/backend-core/src/middleware/passport/sso/oidc.ts @@ -4,7 +4,6 @@ import { ssoCallbackUrl } from "../utils" import { ConfigType, OIDCInnerConfig, - Database, SSOProfile, OIDCStrategyConfiguration, SSOAuthDetails, @@ -157,9 +156,6 @@ export async function fetchStrategyConfig( } } -export async function getCallbackUrl( - db: Database, - config: { callbackURL?: string } -) { - return ssoCallbackUrl(db, config, ConfigType.OIDC) +export async function getCallbackUrl() { + return ssoCallbackUrl(ConfigType.OIDC) } diff --git a/packages/backend-core/src/middleware/passport/utils.ts b/packages/backend-core/src/middleware/passport/utils.ts index 6eb3bc29d1..7e0d3863a0 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 "../../context" -import { getScopedConfig } from "../../db" -import { ConfigType, Database } from "@budibase/types" +import { getTenantId, isMultiTenant } from "../../context" +import * as configs from "../../configs" +import { ConfigType, GoogleInnerConfig } from "@budibase/types" /** * Utility to handle authentication errors. @@ -19,17 +19,14 @@ export function authError(done: Function, message: string, err?: any) { } export async function ssoCallbackUrl( - db: Database, - config?: { callbackURL?: string }, - type?: ConfigType + type: ConfigType, + config?: GoogleInnerConfig ) { // incase there is a callback URL from before - if (config && config.callbackURL) { - return config.callbackURL + if (config && (config as GoogleInnerConfig).callbackURL) { + return (config as GoogleInnerConfig).callbackURL as string } - const publicConfig = await getScopedConfig(db, { - type: ConfigType.SETTINGS, - }) + const settingsConfig = await configs.getSettingsConfig() let callbackUrl = `/api/global/auth` if (isMultiTenant()) { @@ -37,5 +34,5 @@ export async function ssoCallbackUrl( } callbackUrl += `/${type}/callback` - return `${publicConfig.platformUrl}${callbackUrl}` + return `${settingsConfig.platformUrl}${callbackUrl}` } diff --git a/packages/backend-core/tests/utilities/mocks/licenses.ts b/packages/backend-core/tests/utilities/mocks/licenses.ts index 210c03b900..2ca41616e4 100644 --- a/packages/backend-core/tests/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/utilities/mocks/licenses.ts @@ -70,6 +70,10 @@ export const useBackups = () => { return useFeature(Feature.APP_BACKUPS) } +export const useEnforceableSSO = () => { + return useFeature(Feature.ENFORCEABLE_SSO) +} + export const useGroups = () => { return useFeature(Feature.USER_GROUPS) } diff --git a/packages/backend-core/tests/utilities/structures/sso.ts b/packages/backend-core/tests/utilities/structures/sso.ts index df2a237d5f..a5957c9233 100644 --- a/packages/backend-core/tests/utilities/structures/sso.ts +++ b/packages/backend-core/tests/utilities/structures/sso.ts @@ -72,6 +72,7 @@ export function oidcConfig(): OIDCInnerConfig { configUrl: "http://someconfigurl", clientID: generator.string(), clientSecret: generator.string(), + scopes: [], } } diff --git a/packages/bbui/package.json b/packages/bbui/package.json index b697a532ad..ca63265dda 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.18-alpha.12", + "version": "2.3.18-alpha.13", "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.18-alpha.12", + "@budibase/string-templates": "2.3.18-alpha.13", "@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 b3afb4de2a..1d75b3874d 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.12", - "@budibase/client": "2.3.18-alpha.12", - "@budibase/frontend-core": "2.3.18-alpha.12", - "@budibase/string-templates": "2.3.18-alpha.12", + "@budibase/bbui": "2.3.18-alpha.13", + "@budibase/client": "2.3.18-alpha.13", + "@budibase/frontend-core": "2.3.18-alpha.13", + "@budibase/string-templates": "2.3.18-alpha.13", "@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/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 80122c23a5..06e09e4fee 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -79,67 +79,71 @@ - {/if} - - { - formData = { - ...formData, - username: e.detail, - } - }} - validate={() => { - let fieldError = { - username: !formData.username - ? "Please enter a valid email" - : undefined, - } - errors = handleError({ ...errors, ...fieldError }) - }} - error={errors.username} - /> - { - formData = { - ...formData, - password: e.detail, - } - }} - validate={() => { - let fieldError = { - password: !formData.password - ? "Please enter your password" - : undefined, - } - errors = handleError({ ...errors, ...fieldError }) - }} - error={errors.password} - /> - - - - - - - + {#if !$organisation.isSSOEnforced} + + + { + formData = { + ...formData, + username: e.detail, + } + }} + validate={() => { + let fieldError = { + username: !formData.username + ? "Please enter a valid email" + : undefined, + } + errors = handleError({ ...errors, ...fieldError }) + }} + error={errors.username} + /> + { + formData = { + ...formData, + password: e.detail, + } + }} + validate={() => { + let fieldError = { + password: !formData.password + ? "Please enter your password" + : undefined, + } + errors = handleError({ ...errors, ...fieldError }) + }} + error={errors.password} + /> + + {/if} + {#if !$organisation.isSSOEnforced} + + + + + + + {/if} {#if cloud} diff --git a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte index 608ca271c0..e5196a1cbf 100644 --- a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte @@ -22,10 +22,11 @@ Tags, Icon, Helpers, + Link, } from "@budibase/bbui" import { onMount } from "svelte" import { API } from "api" - import { organisation, admin } from "stores/portal" + import { organisation, admin, licensing } from "stores/portal" const ConfigTypes = { Google: "google", @@ -34,6 +35,8 @@ const HasSpacesRegex = /[\\"\s]/ + $: enforcedSSO = $organisation.isSSOEnforced + // Some older google configs contain a manually specified value - retain the functionality to edit the field // When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change $: googleCallbackUrl = undefined @@ -154,6 +157,11 @@ iconDropdownOptions.unshift({ label: fileName, value: fileName }) } + async function toggleIsSSOEnforced() { + const value = $organisation.isSSOEnforced + await organisation.save({ isSSOEnforced: !value }) + } + async function save(docs) { let calls = [] // Only if the user has provided an image, upload it @@ -316,6 +324,49 @@ Authentication Add additional authentication methods from the options below + + + Single Sign-On URL + + Use the following link to access your configured identity provider. + + + + + + + +
+
+
+ Enforce Single Sign-On +
+ {#if !$licensing.enforceableSSO} + + Business plan + + {/if} +
+ {#if $licensing.enforceableSSO} + + {/if} +
+ + Require SSO authentication for all users. It is recommended to read the + help documentation before enabling this feature. + +
{#if providers.google} @@ -546,7 +597,24 @@ input[type="file"] { display: none; } - + .sso-link-icon { + padding-top: 4px; + margin-left: 3px; + } + .sso-link { + margin-top: 12px; + display: flex; + flex-direction: row; + align-items: center; + } + .enforce-sso-title { + margin-right: 10px; + } + .enforce-sso-heading-container { + display: flex; + flex-direction: row; + align-items: start; + } .provider-title { display: flex; flex-direction: row; diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index 8e4d6e738c..bba046ce10 100644 --- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte +++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte @@ -24,6 +24,7 @@ } const values = writable({ + isSSOEnforced: $organisation.isSSOEnforced, company: $organisation.company, platformUrl: $organisation.platformUrl, analyticsEnabled: $organisation.analyticsEnabled, @@ -54,6 +55,7 @@ } const config = { + isSSOEnforced: $values.isSSOEnforced, company: $values.company ?? "", platformUrl: $values.platformUrl ?? "", analyticsEnabled: $values.analyticsEnabled, diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js index 4215e92741..df63e9455b 100644 --- a/packages/builder/src/stores/portal/licensing.js +++ b/packages/builder/src/stores/portal/licensing.js @@ -63,6 +63,9 @@ export const createLicensingStore = () => { const environmentVariablesEnabled = license.features.includes( Constants.Features.ENVIRONMENT_VARIABLES ) + const enforceableSSO = license.features.includes( + Constants.Features.ENFORCEABLE_SSO + ) const auditLogsEnabled = license.features.includes( Constants.Features.AUDIT_LOGS @@ -76,6 +79,7 @@ export const createLicensingStore = () => { backupsEnabled, environmentVariablesEnabled, auditLogsEnabled, + enforceableSSO, } }) }, diff --git a/packages/builder/src/stores/portal/organisation.js b/packages/builder/src/stores/portal/organisation.js index 9709578fa2..c8b62bb2bc 100644 --- a/packages/builder/src/stores/portal/organisation.js +++ b/packages/builder/src/stores/portal/organisation.js @@ -11,6 +11,7 @@ const DEFAULT_CONFIG = { google: undefined, oidcCallbackUrl: "", googleCallbackUrl: "", + isSSOEnforced: false, } export function createOrganisationStore() { @@ -19,8 +20,8 @@ export function createOrganisationStore() { async function init() { const tenantId = get(auth).tenantId - const tenant = await API.getTenantConfig(tenantId) - set({ ...DEFAULT_CONFIG, ...tenant.config, _rev: tenant._rev }) + const settingsConfigDoc = await API.getTenantConfig(tenantId) + set({ ...DEFAULT_CONFIG, ...settingsConfigDoc.config }) } async function save(config) { @@ -33,7 +34,6 @@ export function createOrganisationStore() { await API.saveConfig({ type: "settings", config: { ...get(store), ...config }, - _rev: get(store)._rev, }) await init() } diff --git a/packages/cli/package.json b/packages/cli/package.json index 5bc10322c1..157219ea7a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "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.18-alpha.12", - "@budibase/string-templates": "2.3.18-alpha.12", - "@budibase/types": "2.3.18-alpha.12", + "@budibase/backend-core": "2.3.18-alpha.13", + "@budibase/string-templates": "2.3.18-alpha.13", + "@budibase/types": "2.3.18-alpha.13", "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 c18aa46812..b5926feaae 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "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.18-alpha.12", - "@budibase/frontend-core": "2.3.18-alpha.12", - "@budibase/string-templates": "2.3.18-alpha.12", + "@budibase/bbui": "2.3.18-alpha.13", + "@budibase/frontend-core": "2.3.18-alpha.13", + "@budibase/string-templates": "2.3.18-alpha.13", "@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 bc7a82d0a6..c29e9cc785 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "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.18-alpha.12", + "@budibase/bbui": "2.3.18-alpha.13", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index 8c7103a88f..09b7a00aec 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -116,6 +116,7 @@ export const Features = { BACKUPS: "appBackups", ENVIRONMENT_VARIABLES: "environmentVariables", AUDIT_LOGS: "auditLogs", + ENFORCEABLE_SSO: "enforceableSSO", } // Role IDs diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8199e84495..b1f68d3b27 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index dbf6a7ec13..81a9d02faf 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.18-alpha.12", + "version": "2.3.18-alpha.13", "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.18-alpha.12", - "@budibase/client": "2.3.18-alpha.12", - "@budibase/pro": "2.3.18-alpha.12", - "@budibase/string-templates": "2.3.18-alpha.12", - "@budibase/types": "2.3.18-alpha.12", + "@budibase/backend-core": "2.3.18-alpha.13", + "@budibase/client": "2.3.18-alpha.13", + "@budibase/pro": "2.3.18-alpha.13", + "@budibase/string-templates": "2.3.18-alpha.13", + "@budibase/types": "2.3.18-alpha.13", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/src/api/controllers/cloud.ts b/packages/server/src/api/controllers/cloud.ts index 7f43f21fc9..7be00e3a1d 100644 --- a/packages/server/src/api/controllers/cloud.ts +++ b/packages/server/src/api/controllers/cloud.ts @@ -58,7 +58,7 @@ export async function exportApps(ctx: Ctx) { } async function checkHasBeenImported() { - if (!env.SELF_HOSTED || env.MULTI_TENANCY) { + if (!env.SELF_HOSTED) { return true } const apps = await dbCore.getAllApps({ all: true }) @@ -72,7 +72,7 @@ export async function hasBeenImported(ctx: Ctx) { } export async function importApps(ctx: Ctx) { - if (!env.SELF_HOSTED || env.MULTI_TENANCY) { + if (!env.SELF_HOSTED) { ctx.throw(400, "Importing only allowed in self hosted environments.") } const beenImported = await checkHasBeenImported() diff --git a/packages/server/src/api/routes/tests/cloud.seq.spec.ts b/packages/server/src/api/routes/tests/cloud.spec.ts similarity index 70% rename from packages/server/src/api/routes/tests/cloud.seq.spec.ts rename to packages/server/src/api/routes/tests/cloud.spec.ts index d9bd6221ad..aad1214a31 100644 --- a/packages/server/src/api/routes/tests/cloud.seq.spec.ts +++ b/packages/server/src/api/routes/tests/cloud.spec.ts @@ -1,3 +1,5 @@ +import { App } from "@budibase/types" + jest.setTimeout(30000) import { AppStatus } from "../../../db/utils" @@ -5,6 +7,7 @@ import { AppStatus } from "../../../db/utils" import * as setup from "./utilities" import { wipeDb } from "./utilities/TestFunctions" +import { tenancy } from "@budibase/backend-core" describe("/cloud", () => { let request = setup.getRequest()! @@ -12,18 +15,10 @@ describe("/cloud", () => { afterAll(setup.afterAll) - beforeAll(() => { + beforeAll(async () => { // Importing is only allowed in self hosted environments - config.modeSelf() - }) - - beforeEach(async () => { await config.init() - }) - - afterEach(async () => { - // clear all mocks - jest.clearAllMocks() + config.modeSelf() }) describe("import", () => { @@ -32,30 +27,28 @@ describe("/cloud", () => { // import will not run await wipeDb() - // get a count of apps before the import - const preImportApps = await request - .get(`/api/applications?status=${AppStatus.ALL}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - // Perform the import const res = await request .post(`/api/cloud/import`) + .set(config.publicHeaders()) .attach("importFile", "src/api/routes/tests/data/export-test.tar.gz") - .set(config.defaultHeaders()) .expect(200) expect(res.body.message).toEqual("Apps successfully imported.") // get a count of apps after the import const postImportApps = await request .get(`/api/applications?status=${AppStatus.ALL}`) - .set(config.defaultHeaders()) + .set(config.publicHeaders()) .expect("Content-Type", /json/) .expect(200) + const apps = postImportApps.body as App[] // There are two apps in the file that was imported so check for this - expect(postImportApps.body.length).toEqual(2) + expect(apps.length).toEqual(2) + // The new tenant id was assigned to the imported apps + expect(tenancy.getTenantIDFromAppID(apps[0].appId)).toBe( + config.getTenantId() + ) }) }) }) diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts index 3f57c1e8ef..5a1ed5210e 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts @@ -2,7 +2,6 @@ import * as rowController from "../../../controllers/row" import * as appController from "../../../controllers/application" import { AppStatus } from "../../../../db/utils" import { roles, tenancy, context } from "@budibase/backend-core" -import { TENANT_ID } from "../../../../tests/utilities/structures" import env from "../../../../environment" import { db } from "@budibase/backend-core" import Nano from "@budibase/nano" @@ -33,7 +32,7 @@ export const getAllTableRows = async (config: any) => { } export const clearAllApps = async ( - tenantId = TENANT_ID, + tenantId: string, exceptions: Array = [] ) => { await tenancy.doInTenant(tenantId, async () => { diff --git a/packages/server/src/integrations/googlesheets.ts b/packages/server/src/integrations/googlesheets.ts index d3caf0b944..2b54123bdb 100644 --- a/packages/server/src/integrations/googlesheets.ts +++ b/packages/server/src/integrations/googlesheets.ts @@ -11,8 +11,7 @@ import { OAuth2Client } from "google-auth-library" import { buildExternalTableId } from "./utils" import { DataSourceOperation, FieldTypes } from "../constants" import { GoogleSpreadsheet } from "google-spreadsheet" -import env from "../environment" -import { tenancy, db as dbCore, constants } from "@budibase/backend-core" +import { configs, HTTPError } from "@budibase/backend-core" const fetch = require("node-fetch") interface GoogleSheetsConfig { @@ -173,16 +172,9 @@ class GoogleSheetsIntegration implements DatasourcePlus { async connect() { try { // Initialise oAuth client - const db = tenancy.getGlobalDB() - let googleConfig = await dbCore.getScopedConfig(db, { - type: constants.Config.GOOGLE, - }) - + let googleConfig = await configs.getGoogleConfig() if (!googleConfig) { - googleConfig = { - clientID: env.GOOGLE_CLIENT_ID, - clientSecret: env.GOOGLE_CLIENT_SECRET, - } + throw new HTTPError("Google config not found", 400) } const oauthClient = new OAuth2Client({ diff --git a/packages/server/src/migrations/functions/backfill/global/configs.ts b/packages/server/src/migrations/functions/backfill/global/configs.ts index 7eaa987bc7..1b76727bbe 100644 --- a/packages/server/src/migrations/functions/backfill/global/configs.ts +++ b/packages/server/src/migrations/functions/backfill/global/configs.ts @@ -1,4 +1,9 @@ -import { events, db as dbUtils } from "@budibase/backend-core" +import { + events, + DocumentType, + SEPARATOR, + UNICODE_MAX, +} from "@budibase/backend-core" import { Config, isSMTPConfig, @@ -9,15 +14,16 @@ import { } from "@budibase/types" import env from "./../../../../environment" +export const getConfigParams = () => { + return { + include_docs: true, + startkey: `${DocumentType.CONFIG}${SEPARATOR}`, + endkey: `${DocumentType.CONFIG}${SEPARATOR}${UNICODE_MAX}`, + } +} + const getConfigs = async (globalDb: any): Promise => { - const response = await globalDb.allDocs( - dbUtils.getConfigParams( - {}, - { - include_docs: true, - } - ) - ) + const response = await globalDb.allDocs(getConfigParams()) return response.rows.map((row: any) => row.doc) } diff --git a/packages/server/src/migrations/functions/tests/userEmailViewCasing.spec.js b/packages/server/src/migrations/functions/tests/userEmailViewCasing.spec.js index c0066b1c71..15ddcfd1ef 100644 --- a/packages/server/src/migrations/functions/tests/userEmailViewCasing.spec.js +++ b/packages/server/src/migrations/functions/tests/userEmailViewCasing.spec.js @@ -8,9 +8,8 @@ jest.mock("@budibase/backend-core", () => { } } }) -const { tenancy, db: dbCore } = require("@budibase/backend-core") +const { context, db: dbCore } = require("@budibase/backend-core") const TestConfig = require("../../../tests/utilities/TestConfiguration") -const { TENANT_ID } = require("../../../tests/utilities/structures") // mock email view creation @@ -26,8 +25,8 @@ describe("run", () => { afterAll(config.end) it("runs successfully", async () => { - await tenancy.doInTenant(TENANT_ID, async () => { - const globalDb = tenancy.getGlobalDB() + await config.doInTenant(async () => { + const globalDb = context.getGlobalDB() await migration.run(globalDb) expect(dbCore.createNewUserEmailView).toHaveBeenCalledTimes(1) }) diff --git a/packages/server/src/migrations/tests/helpers.ts b/packages/server/src/migrations/tests/helpers.ts index 17658d5290..35831a2fd0 100644 --- a/packages/server/src/migrations/tests/helpers.ts +++ b/packages/server/src/migrations/tests/helpers.ts @@ -1,7 +1,7 @@ // Mimic configs test configuration from worker, creation configs directly in database import * as structures from "./structures" -import { db } from "@budibase/backend-core" +import { configs } from "@budibase/backend-core" import { Config } from "@budibase/types" export const saveSettingsConfig = async (globalDb: any) => { @@ -25,7 +25,7 @@ export const saveSmtpConfig = async (globalDb: any) => { } const saveConfig = async (config: Config, globalDb: any) => { - config._id = db.generateConfigID({ type: config.type }) + config._id = configs.generateConfigID(config.type) let response try { diff --git a/packages/server/src/migrations/tests/structures.ts b/packages/server/src/migrations/tests/structures.ts index bd48bf63cd..b075c04f5c 100644 --- a/packages/server/src/migrations/tests/structures.ts +++ b/packages/server/src/migrations/tests/structures.ts @@ -20,6 +20,7 @@ export const oidc = (conf?: OIDCConfig): OIDCConfig => { name: "Active Directory", uuid: utils.newid(), activated: true, + scopes: [], ...conf, }, ], diff --git a/packages/server/src/tests/jestEnv.ts b/packages/server/src/tests/jestEnv.ts index c567b260b3..7727bb6007 100644 --- a/packages/server/src/tests/jestEnv.ts +++ b/packages/server/src/tests/jestEnv.ts @@ -8,3 +8,4 @@ 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" +process.env.PLATFORM_URL = "http://localhost:10000" diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 88545dbcbb..e9b770229f 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -21,7 +21,6 @@ import { basicScreen, basicLayout, basicWebhook, - TENANT_ID, } from "./structures" import { constants, @@ -41,8 +40,8 @@ import { generateUserMetadataID } from "../../db/utils" import { startup } from "../../startup" import supertest from "supertest" import { + App, AuthToken, - Database, Datasource, Row, SourceName, @@ -63,7 +62,7 @@ class TestConfiguration { started: boolean appId: string | null allApps: any[] - app: any + app?: App prodApp: any prodAppId: any user: any @@ -73,7 +72,7 @@ class TestConfiguration { linkedTable: any automation: any datasource: any - tenantId: string | null + tenantId?: string defaultUserValues: DefaultUserValues constructor(openServer = true) { @@ -89,7 +88,6 @@ class TestConfiguration { } this.appId = null this.allApps = [] - this.tenantId = null this.defaultUserValues = this.populateDefaultUserValues() } @@ -154,19 +152,10 @@ class TestConfiguration { // use a new id as the name to avoid name collisions async init(appName = newid()) { - this.defaultUserValues = this.populateDefaultUserValues() - if (context.isMultiTenant()) { - this.tenantId = structures.tenant.id() - } - if (!this.started) { await startup() } - this.user = await this.globalUser() - this.globalUserId = this.user._id - this.userMetadataId = generateUserMetadataID(this.globalUserId) - - return this.createApp(appName) + return this.newTenant(appName) } end() { @@ -182,24 +171,22 @@ class TestConfiguration { } // MODES - #setMultiTenancy = (value: boolean) => { + setMultiTenancy = (value: boolean) => { env._set("MULTI_TENANCY", value) coreEnv._set("MULTI_TENANCY", value) } - #setSelfHosted = (value: boolean) => { + setSelfHosted = (value: boolean) => { env._set("SELF_HOSTED", value) coreEnv._set("SELF_HOSTED", value) } modeCloud = () => { - this.#setSelfHosted(false) - this.#setMultiTenancy(true) + this.setSelfHosted(false) } modeSelf = () => { - this.#setSelfHosted(true) - this.#setMultiTenancy(false) + this.setSelfHosted(true) } // UTILS @@ -354,6 +341,8 @@ class TestConfiguration { }) } + // HEADERS + defaultHeaders(extras = {}) { const tenantId = this.getTenantId() const authObj: AuthToken = { @@ -374,6 +363,7 @@ class TestConfiguration { `${constants.Cookie.CurrentApp}=${appToken}`, ], [constants.Header.CSRF_TOKEN]: this.defaultUserValues.csrfToken, + Host: this.tenantHost(), ...extras, } @@ -383,10 +373,6 @@ class TestConfiguration { return headers } - getTenantId() { - return this.tenantId || TENANT_ID - } - publicHeaders({ prodApp = true } = {}) { const appId = prodApp ? this.prodAppId : this.appId @@ -397,9 +383,7 @@ class TestConfiguration { headers[constants.Header.APP_ID] = appId } - if (this.tenantId) { - headers[constants.Header.TENANT_ID] = this.tenantId - } + headers[constants.Header.TENANT_ID] = this.getTenantId() return headers } @@ -413,6 +397,34 @@ class TestConfiguration { return this.login({ email, roleId, builder, prodApp }) } + // TENANCY + + tenantHost() { + const tenantId = this.getTenantId() + const platformHost = new URL(coreEnv.PLATFORM_URL).host.split(":")[0] + return `${tenantId}.${platformHost}` + } + + getTenantId() { + if (!this.tenantId) { + throw new Error("no test tenant id - init has not been called") + } + return this.tenantId + } + + async newTenant(appName = newid()): Promise { + this.defaultUserValues = this.populateDefaultUserValues() + this.tenantId = structures.tenant.id() + this.user = await this.globalUser() + this.globalUserId = this.user._id + this.userMetadataId = generateUserMetadataID(this.globalUserId) + return this.createApp(appName) + } + + doInTenant(task: any) { + return context.doInTenant(this.getTenantId(), task) + } + // API async generateApiKey(userId = this.defaultUserValues.globalUserId) { @@ -432,7 +444,7 @@ class TestConfiguration { } // APP - async createApp(appName: string) { + async createApp(appName: string): Promise { // create dev app // clear any old app this.appId = null @@ -442,7 +454,7 @@ class TestConfiguration { null, controllers.app.create ) - this.appId = this.app.appId + this.appId = this.app?.appId! }) return await context.doInAppContext(this.appId, async () => { // create production app diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index c1959dc791..e38c0a5275 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -13,8 +13,6 @@ import { const { v4: uuidv4 } = require("uuid") -export const TENANT_ID = "default" - export function basicTable() { return { name: "TestTable", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 15a5b3b192..be94307f0d 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.18-alpha.12": - version "2.3.18-alpha.12" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.12.tgz#ad1b16be64b78b596af2b5f75647c32e8f6f101a" - integrity sha512-E1NEO+/sNkkRqn/xk9XQmFBO9/dl27w9EB0QGztti/16JV9NgxyDQCJIdGwlD08s1y/lUwOKk0TkSZJs+CTYDw== +"@budibase/backend-core@2.3.18-alpha.13": + version "2.3.18-alpha.13" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.13.tgz#b797d7a4d30ff7f21e473334edb4086f5818830e" + integrity sha512-c6d0xCRgLlPeX1euAoQuoDOkMkDGQy/miBx/Z8xyU9bzDDTNqBTogxpxsNf8DdZG7EMJhsJlCUvb26Onz7/50A== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.12" + "@budibase/types" "2.3.18-alpha.13" "@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.18-alpha.12": - version "2.3.18-alpha.12" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.12.tgz#be552b3a9f5850e746081540d6586aae69147bec" - integrity sha512-M3b0njzSi47KH6uaQfYPoA2KWrjPiwcU3ONyaVWXHIktVrIKtYaFwOLBr/dmWGfMrL2297SSqg7V4DTaLyAhnw== +"@budibase/pro@2.3.18-alpha.13": + version "2.3.18-alpha.13" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.13.tgz#9f3dd0339184a58609c726f3f2b4580cf802dc78" + integrity sha512-pRroVVFGITFsFtzzH6LgzuaUBKldvFQxTgO7K6dYjjE7xwTCnZCg0E4L2Ew/JIY39UN2WEb5bwdt3+pqPH7Dmg== dependencies: - "@budibase/backend-core" "2.3.18-alpha.12" - "@budibase/types" "2.3.18-alpha.12" + "@budibase/backend-core" "2.3.18-alpha.13" + "@budibase/types" "2.3.18-alpha.13" "@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.18-alpha.12": - version "2.3.18-alpha.12" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.12.tgz#a63eb978ccc7e55c209b3e9d71f9aecf7facc0d1" - integrity sha512-27o2BmI/HXIR3frZ8FtqHgAe1hd8jPIzgPaEhKrQiYJ/opUVccqupx9ld75Hyk9E6cdXu0UF0/+LxPpUmMugag== +"@budibase/types@2.3.18-alpha.13": + version "2.3.18-alpha.13" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.13.tgz#e80cbf79249ed5cf8fe86e294d0b1e25e0a839ee" + integrity sha512-fmgpwMMGkbPOObmFnZZH8iKelycmhvBydGQuPEmIk1c449ysfiKF2Strnca6MaY1XtskfRz/nNdWGdGS1HhmFw== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 1baef9fcf0..41ea0cf30d 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "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 e3972ab5ea..ae19e2eb5d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.18-alpha.12", + "version": "2.3.18-alpha.13", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/types/src/api/web/global/configs.ts b/packages/types/src/api/web/global/configs.ts new file mode 100644 index 0000000000..1476d13cee --- /dev/null +++ b/packages/types/src/api/web/global/configs.ts @@ -0,0 +1,23 @@ +import { SettingsConfig, SettingsInnerConfig } from "../../../documents" + +/** + * Settings that aren't stored in the database - enriched at runtime. + */ +export interface PublicSettingsInnerConfig extends SettingsInnerConfig { + google: boolean + oidc: boolean + oidcCallbackUrl: string + googleCallbackUrl: string +} + +export interface GetPublicSettingsResponse extends SettingsConfig { + config: PublicSettingsInnerConfig +} + +export interface PublicOIDCConfig { + logo?: string + name?: string + uuid?: string +} + +export type GetPublicOIDCConfigResponse = PublicOIDCConfig[] diff --git a/packages/types/src/api/web/global/index.ts b/packages/types/src/api/web/global/index.ts index f9eec2a779..21a5de3727 100644 --- a/packages/types/src/api/web/global/index.ts +++ b/packages/types/src/api/web/global/index.ts @@ -1,3 +1,4 @@ export * from "./environmentVariables" export * from "./auditLogs" export * from "./events" +export * from "./configs" diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts index 99dec534b6..f4bba21e0f 100644 --- a/packages/types/src/documents/global/config.ts +++ b/packages/types/src/documents/global/config.ts @@ -5,32 +5,45 @@ export interface Config extends Document { config: any } -export interface SMTPConfig extends Config { - config: { - port: number - host: string - from: string - subject: string - secure: boolean +export interface SMTPInnerConfig { + port: number + host: string + from: string + subject?: string + secure: boolean + auth?: { + user: string + pass: string } + connectionTimeout?: any +} + +export interface SMTPConfig extends Config { + config: SMTPInnerConfig +} + +export interface SettingsInnerConfig { + platformUrl?: string + company?: string + logoUrl?: string // Populated on read + logoUrlEtag?: string + uniqueTenantId?: string + analyticsEnabled?: boolean + isSSOEnforced?: boolean } export interface SettingsConfig extends Config { - config: { - company: string - // Populated on read - logoUrl?: string - logoUrlEtag?: boolean - platformUrl: string - uniqueTenantId?: string - analyticsEnabled?: boolean - } + config: SettingsInnerConfig } export interface GoogleInnerConfig { clientID: string clientSecret: string activated: boolean + /** + * @deprecated read only + */ + callbackURL?: string } export interface GoogleConfig extends Config { @@ -55,6 +68,7 @@ export interface OIDCInnerConfig { name: string uuid: string activated: boolean + scopes: string[] } export interface OIDCConfig extends Config { diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index 7ebd1574ca..9f6090623b 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -3,4 +3,5 @@ export enum Feature { APP_BACKUPS = "appBackups", ENVIRONMENT_VARIABLES = "environmentVariables", AUDIT_LOGS = "auditLogs", + ENFORCEABLE_SSO = "enforceableSSO", } diff --git a/packages/worker/package.json b/packages/worker/package.json index 4d8183fe7f..3f4adc8cf5 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.18-alpha.12", + "version": "2.3.18-alpha.13", "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.18-alpha.12", - "@budibase/pro": "2.3.18-alpha.12", - "@budibase/string-templates": "2.3.18-alpha.12", - "@budibase/types": "2.3.18-alpha.12", + "@budibase/backend-core": "2.3.18-alpha.13", + "@budibase/pro": "2.3.18-alpha.13", + "@budibase/string-templates": "2.3.18-alpha.13", + "@budibase/types": "2.3.18-alpha.13", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 318f69c7ad..92cf014a48 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -2,10 +2,9 @@ import { auth as authCore, constants, context, - db as dbCore, events, - tenancy, utils as utilsCore, + configs, } from "@budibase/backend-core" import { ConfigType, @@ -15,6 +14,7 @@ import { SSOUser, PasswordResetRequest, PasswordResetUpdateRequest, + GoogleInnerConfig, } from "@budibase/types" import env from "../../../environment" @@ -61,8 +61,8 @@ 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") + if (user && (await userSdk.isPreventPasswordActions(user))) { + ctx.throw(400, "Password login is disabled for this user") } return passport.authenticate( @@ -163,8 +163,8 @@ export const datasourceAuth = async (ctx: any, next: any) => { // GOOGLE SSO -export async function googleCallbackUrl(config?: { callbackURL?: string }) { - return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.GOOGLE) +export async function googleCallbackUrl(config?: GoogleInnerConfig) { + return ssoCallbackUrl(ConfigType.GOOGLE, config) } /** @@ -172,12 +172,10 @@ export async function googleCallbackUrl(config?: { callbackURL?: string }) { * On a successful login, you will be redirected to the googleAuth callback route. */ export const googlePreAuth = async (ctx: any, next: any) => { - const db = tenancy.getGlobalDB() - - const config = await dbCore.getScopedConfig(db, { - type: ConfigType.GOOGLE, - workspace: ctx.query.workspace, - }) + const config = await configs.getGoogleConfig() + if (!config) { + return ctx.throw(400, "Google config not found") + } let callbackUrl = await googleCallbackUrl(config) const strategy = await google.strategyFactory( config, @@ -193,12 +191,10 @@ export const googlePreAuth = async (ctx: any, next: any) => { } export const googleCallback = async (ctx: any, next: any) => { - const db = tenancy.getGlobalDB() - - const config = await dbCore.getScopedConfig(db, { - type: ConfigType.GOOGLE, - workspace: ctx.query.workspace, - }) + const config = await configs.getGoogleConfig() + if (!config) { + return ctx.throw(400, "Google config not found") + } const callbackUrl = await googleCallbackUrl(config) const strategy = await google.strategyFactory( config, @@ -221,25 +217,20 @@ export const googleCallback = async (ctx: any, next: any) => { // OIDC SSO -export async function oidcCallbackUrl(config?: { callbackURL?: string }) { - return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.OIDC) +export async function oidcCallbackUrl() { + return ssoCallbackUrl(ConfigType.OIDC) } export const oidcStrategyFactory = async (ctx: any, configId: any) => { - const db = tenancy.getGlobalDB() - const config = await dbCore.getScopedConfig(db, { - type: ConfigType.OIDC, - group: ctx.query.group, - }) + const config = await configs.getOIDCConfig() + if (!config) { + return ctx.throw(400, "OIDC config not found") + } - const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] - let callbackUrl = await oidcCallbackUrl(chosenConfig) + let callbackUrl = await oidcCallbackUrl() //Remote Config - const enrichedConfig = await oidc.fetchStrategyConfig( - chosenConfig, - callbackUrl - ) + const enrichedConfig = await oidc.fetchStrategyConfig(config, callbackUrl) return oidc.strategyFactory(enrichedConfig, userSdk.save) } @@ -247,23 +238,23 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => { * The initial call that OIDC authentication makes to take you to the configured OIDC login screen. * On a successful login, you will be redirected to the oidcAuth callback route. */ -export const oidcPreAuth = async (ctx: any, next: any) => { +export const oidcPreAuth = async (ctx: Ctx, next: any) => { const { configId } = ctx.params + if (!configId) { + ctx.throw(400, "OIDC config id is required") + } const strategy = await oidcStrategyFactory(ctx, configId) setCookie(ctx, configId, Cookie.OIDC_CONFIG) - const db = tenancy.getGlobalDB() - const config = await dbCore.getScopedConfig(db, { - type: ConfigType.OIDC, - group: ctx.query.group, - }) - - const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] + const config = await configs.getOIDCConfigById(configId) + if (!config) { + return ctx.throw(400, "OIDC config not found") + } let authScopes = - chosenConfig.scopes?.length > 0 - ? chosenConfig.scopes + config.scopes?.length > 0 + ? config.scopes : ["profile", "email", "offline_access"] return passport.authenticate(strategy, { diff --git a/packages/worker/src/api/controllers/global/configs.ts b/packages/worker/src/api/controllers/global/configs.ts index 855d766a87..1ea0f52c96 100644 --- a/packages/worker/src/api/controllers/global/configs.ts +++ b/packages/worker/src/api/controllers/global/configs.ts @@ -2,38 +2,32 @@ import * as email from "../../../utilities/email" import env from "../../../environment" import { googleCallbackUrl, oidcCallbackUrl } from "./auth" import { - events, cache, - objectStore, - tenancy, + configs, db as dbCore, env as coreEnv, + events, + objectStore, + tenancy, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" import { - Database, - Config as ConfigDoc, + Config, ConfigType, - SSOType, - GoogleConfig, - OIDCConfig, - SettingsConfig, + Ctx, + GetPublicOIDCConfigResponse, + GetPublicSettingsResponse, isGoogleConfig, isOIDCConfig, isSettingsConfig, isSMTPConfig, - Ctx, UserCtx, } from "@budibase/types" +import * as pro from "@budibase/pro" -const getEventFns = async (db: Database, config: ConfigDoc) => { +const getEventFns = async (config: Config, existing?: Config) => { const fns = [] - let existing - if (config._id) { - existing = await db.get(config._id) - } - if (!existing) { if (isSMTPConfig(config)) { fns.push(events.email.SMTPCreated) @@ -125,21 +119,21 @@ const getEventFns = async (db: Database, config: ConfigDoc) => { return fns } -export async function save(ctx: UserCtx) { - const db = tenancy.getGlobalDB() - const { type, workspace, user, config } = ctx.request.body - let eventFns = await getEventFns(db, ctx.request.body) - // Config does not exist yet - if (!ctx.request.body._id) { - ctx.request.body._id = dbCore.generateConfigID({ - type, - workspace, - user, - }) +export async function save(ctx: UserCtx) { + const body = ctx.request.body + const type = body.type + const config = body.config + + const existingConfig = await configs.getConfig(type) + let eventFns = await getEventFns(ctx.request.body, existingConfig) + + if (existingConfig) { + body._rev = existingConfig._rev } + try { // verify the configuration - switch (type) { + switch (config.type) { case ConfigType.SMTP: await email.verifyConfig(config) break @@ -149,7 +143,8 @@ export async function save(ctx: UserCtx) { } try { - const response = await db.put(ctx.request.body) + body._id = configs.generateConfigID(type) + const response = await configs.save(body) await cache.bustCache(cache.CacheKey.CHECKLIST) await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED) @@ -167,44 +162,11 @@ export async function save(ctx: UserCtx) { } } -export async function fetch(ctx: UserCtx) { - const db = tenancy.getGlobalDB() - const response = await db.allDocs( - dbCore.getConfigParams( - { type: ctx.params.type }, - { - include_docs: true, - } - ) - ) - ctx.body = response.rows.map(row => row.doc) -} - -/** - * Gets the most granular config for a particular configuration type. - * The hierarchy is type -> workspace -> user. - */ export async function find(ctx: UserCtx) { - const db = tenancy.getGlobalDB() - - const { userId, workspaceId } = ctx.query - if (workspaceId && userId) { - const workspace = await db.get(workspaceId as string) - const userInWorkspace = workspace.users.some( - (workspaceUser: any) => workspaceUser === userId - ) - if (!ctx.user!.admin && !userInWorkspace) { - ctx.throw(400, `User is not in specified workspace: ${workspace}.`) - } - } - try { // Find the config with the most granular scope based on context - const scopedConfig = await dbCore.getScopedFullConfig(db, { - type: ctx.params.type, - user: userId, - workspace: workspaceId, - }) + const type = ctx.params.type + const scopedConfig = await configs.getConfig(type) if (scopedConfig) { ctx.body = scopedConfig @@ -217,85 +179,70 @@ export async function find(ctx: UserCtx) { } } -export async function publicOidc(ctx: Ctx) { - const db = tenancy.getGlobalDB() +export async function publicOidc(ctx: Ctx) { try { // Find the config with the most granular scope based on context - const oidcConfig: OIDCConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.OIDC, - }) + const config = await configs.getOIDCConfig() - if (!oidcConfig) { - ctx.body = {} + if (!config) { + ctx.body = [] } else { - ctx.body = oidcConfig.config.configs.map(config => ({ - logo: config.logo, - name: config.name, - uuid: config.uuid, - })) + ctx.body = [ + { + logo: config.logo, + name: config.name, + uuid: config.uuid, + }, + ] } } catch (err: any) { ctx.throw(err.status, err) } } -export async function publicSettings(ctx: Ctx) { - const db = tenancy.getGlobalDB() - +export async function publicSettings( + ctx: Ctx +) { try { - // Find the config with the most granular scope based on context - const publicConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.SETTINGS, - }) - - const googleConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.GOOGLE, - }) - - const oidcConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.OIDC, - }) - - let config - if (!publicConfig) { - config = { - config: {}, - } - } else { - config = publicConfig - } - - // enrich the logo url - // empty url means deleted - if (config.config.logoUrl && config.config.logoUrl !== "") { - config.config.logoUrl = objectStore.getGlobalFileUrl( + // settings + const configDoc = await configs.getSettingsConfigDoc() + const config = configDoc.config + // enrich the logo url - empty url means deleted + if (config.logoUrl && config.logoUrl !== "") { + config.logoUrl = objectStore.getGlobalFileUrl( "settings", "logoUrl", - config.config.logoUrlEtag + config.logoUrlEtag ) } - // google button flag - if (googleConfig && googleConfig.config) { - // activated by default for configs pre-activated flag - config.config.google = - googleConfig.config.activated == null || googleConfig.config.activated - } else { - config.config.google = false + // google + const googleConfig = await configs.getGoogleConfig() + const preActivated = googleConfig?.activated == null + const google = preActivated || !!googleConfig?.activated + const _googleCallbackUrl = await googleCallbackUrl(googleConfig) + + // oidc + const oidcConfig = await configs.getOIDCConfig() + const oidc = oidcConfig?.activated || false + const _oidcCallbackUrl = await oidcCallbackUrl() + + // sso enforced + const isSSOEnforced = await pro.features.isSSOEnforced({ config }) + + ctx.body = { + type: ConfigType.SETTINGS, + _id: configDoc._id, + _rev: configDoc._rev, + config: { + ...config, + google, + oidc, + isSSOEnforced, + oidcCallbackUrl: _oidcCallbackUrl, + googleCallbackUrl: _googleCallbackUrl, + }, } - - // callback urls - config.config.oidcCallbackUrl = await oidcCallbackUrl() - config.config.googleCallbackUrl = await googleCallbackUrl() - - // oidc button flag - if (oidcConfig && oidcConfig.config) { - config.config.oidc = oidcConfig.config.configs[0].activated - } else { - config.config.oidc = false - } - - ctx.body = config } catch (err: any) { ctx.throw(err.status, err) } @@ -319,12 +266,11 @@ export async function upload(ctx: UserCtx) { }) // add to configuration structure - // TODO: right now this only does a global level - const db = tenancy.getGlobalDB() - let cfgStructure = await dbCore.getScopedFullConfig(db, { type }) - if (!cfgStructure) { - cfgStructure = { - _id: dbCore.generateConfigID({ type }), + let config = await configs.getConfig(type) + if (!config) { + config = { + _id: configs.generateConfigID(type), + type, config: {}, } } @@ -332,14 +278,14 @@ export async function upload(ctx: UserCtx) { // save the Etag for cache bursting const etag = result.ETag if (etag) { - cfgStructure.config[`${name}Etag`] = etag.replace(/"/g, "") + config.config[`${name}Etag`] = etag.replace(/"/g, "") } // save the file key - cfgStructure.config[`${name}`] = key + config.config[`${name}`] = key // write back to db - await db.put(cfgStructure) + await configs.save(config) ctx.body = { message: "File has been uploaded and url stored to config.", @@ -360,7 +306,6 @@ export async function destroy(ctx: UserCtx) { } export async function configChecklist(ctx: Ctx) { - const db = tenancy.getGlobalDB() const tenantId = tenancy.getTenantId() try { @@ -375,19 +320,13 @@ export async function configChecklist(ctx: Ctx) { } // They have set up SMTP - const smtpConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.SMTP, - }) + const smtpConfig = await configs.getSMTPConfig() // They have set up Google Auth - const googleConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.GOOGLE, - }) + const googleConfig = await configs.getGoogleConfig() // They have set up OIDC - const oidcConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.OIDC, - }) + const oidcConfig = await configs.getOIDCConfig() // They have set up a global user const userExists = await checkAnyUserExists() diff --git a/packages/worker/src/api/routes/global/configs.ts b/packages/worker/src/api/routes/global/configs.ts index 38a31a28e6..922bcea212 100644 --- a/packages/worker/src/api/routes/global/configs.ts +++ b/packages/worker/src/api/routes/global/configs.ts @@ -104,13 +104,7 @@ router controller.save ) .delete("/api/global/configs/:id/:rev", auth.adminOnly, controller.destroy) - .get("/api/global/configs", controller.fetch) .get("/api/global/configs/checklist", controller.configChecklist) - .get( - "/api/global/configs/all/:type", - buildConfigGetValidation(), - controller.fetch - ) .get("/api/global/configs/public", controller.publicSettings) .get("/api/global/configs/public/oidc", controller.publicOidc) .get("/api/global/configs/:type", buildConfigGetValidation(), controller.find) 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 81964c166d..9b5392fc73 100644 --- a/packages/worker/src/api/routes/global/tests/auth.spec.ts +++ b/packages/worker/src/api/routes/global/tests/auth.spec.ts @@ -110,7 +110,7 @@ describe("/api/global/auth", () => { ) expect(response.body).toEqual({ - message: "SSO user cannot login using password", + message: "Password login is disabled for this user", status: 400, }) } @@ -175,7 +175,7 @@ describe("/api/global/auth", () => { ) expect(res.body).toEqual({ - message: "SSO user cannot reset password", + message: "Password reset is disabled for this user", status: 400, error: { code: "http", 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 39ad74d295..892fe8a67b 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,8 @@ jest.mock("nodemailer") import { TestConfiguration, structures, mocks } from "../../../../tests" mocks.email.mock() -import { Config, events } from "@budibase/backend-core" +import { events } from "@budibase/backend-core" +import { GetPublicSettingsResponse, Config, ConfigType } from "@budibase/types" describe("configs", () => { const config = new TestConfiguration() @@ -19,22 +20,29 @@ describe("configs", () => { await config.afterAll() }) - describe("post /api/global/configs", () => { - const saveConfig = async (conf: any, _id?: string, _rev?: string) => { - const data = { - ...conf, - _id, - _rev, - } - - const res = await config.api.configs.saveConfig(data) - - return { - ...data, - ...res.body, - } + const saveConfig = async (conf: Config, _id?: string, _rev?: string) => { + const data = { + ...conf, + _id, + _rev, } + const res = await config.api.configs.saveConfig(data) + return { + ...data, + ...res.body, + } + } + const saveSettingsConfig = async ( + conf?: any, + _id?: string, + _rev?: string + ) => { + const settingsConfig = structures.configs.settings(conf) + return saveConfig(settingsConfig, _id, _rev) + } + + describe("POST /api/global/configs", () => { describe("google", () => { const saveGoogleConfig = async ( conf?: any, @@ -49,20 +57,20 @@ describe("configs", () => { it("should create activated google config", async () => { await saveGoogleConfig() expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.GOOGLE) + expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE) expect(events.auth.SSODeactivated).not.toBeCalled() expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.GOOGLE) - await config.deleteConfig(Config.GOOGLE) + expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE) + await config.deleteConfig(ConfigType.GOOGLE) }) it("should create deactivated google config", async () => { await saveGoogleConfig({ activated: false }) expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.GOOGLE) + expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE) expect(events.auth.SSOActivated).not.toBeCalled() expect(events.auth.SSODeactivated).not.toBeCalled() - await config.deleteConfig(Config.GOOGLE) + await config.deleteConfig(ConfigType.GOOGLE) }) }) @@ -76,11 +84,11 @@ describe("configs", () => { googleConf._rev ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.GOOGLE) + expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE) expect(events.auth.SSOActivated).not.toBeCalled() expect(events.auth.SSODeactivated).toBeCalledTimes(1) - expect(events.auth.SSODeactivated).toBeCalledWith(Config.GOOGLE) - await config.deleteConfig(Config.GOOGLE) + expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.GOOGLE) + await config.deleteConfig(ConfigType.GOOGLE) }) it("should update google config to activated", async () => { @@ -92,11 +100,11 @@ describe("configs", () => { googleConf._rev ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.GOOGLE) + expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE) expect(events.auth.SSODeactivated).not.toBeCalled() expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.GOOGLE) - await config.deleteConfig(Config.GOOGLE) + expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE) + await config.deleteConfig(ConfigType.GOOGLE) }) }) }) @@ -115,20 +123,20 @@ describe("configs", () => { it("should create activated OIDC config", async () => { await saveOIDCConfig() expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.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) + expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC) + await config.deleteConfig(ConfigType.OIDC) }) it("should create deactivated OIDC config", async () => { await saveOIDCConfig({ activated: false }) expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC) expect(events.auth.SSOActivated).not.toBeCalled() expect(events.auth.SSODeactivated).not.toBeCalled() - await config.deleteConfig(Config.OIDC) + await config.deleteConfig(ConfigType.OIDC) }) }) @@ -142,11 +150,11 @@ describe("configs", () => { oidcConf._rev ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.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) + expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.OIDC) + await config.deleteConfig(ConfigType.OIDC) }) it("should update OIDC config to activated", async () => { @@ -158,11 +166,11 @@ describe("configs", () => { oidcConf._rev ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.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) + expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC) + await config.deleteConfig(ConfigType.OIDC) }) }) }) @@ -179,11 +187,11 @@ describe("configs", () => { describe("create", () => { it("should create SMTP config", async () => { - await config.deleteConfig(Config.SMTP) + await config.deleteConfig(ConfigType.SMTP) await saveSMTPConfig() expect(events.email.SMTPUpdated).not.toBeCalled() expect(events.email.SMTPCreated).toBeCalledTimes(1) - await config.deleteConfig(Config.SMTP) + await config.deleteConfig(ConfigType.SMTP) }) }) @@ -194,24 +202,15 @@ describe("configs", () => { 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) + await config.deleteConfig(ConfigType.SMTP) }) }) }) describe("settings", () => { - const saveSettingsConfig = async ( - conf?: any, - _id?: string, - _rev?: string - ) => { - const settingsConfig = structures.configs.settings(conf) - return saveConfig(settingsConfig, _id, _rev) - } - describe("create", () => { it("should create settings config with default settings", async () => { - await config.deleteConfig(Config.SETTINGS) + await config.deleteConfig(ConfigType.SETTINGS) await saveSettingsConfig() @@ -222,7 +221,7 @@ describe("configs", () => { it("should create settings config with non-default settings", async () => { config.selfHosted() - await config.deleteConfig(Config.SETTINGS) + await config.deleteConfig(ConfigType.SETTINGS) const conf = { company: "acme", logoUrl: "http://example.com", @@ -241,7 +240,7 @@ describe("configs", () => { describe("update", () => { it("should update settings config", async () => { config.selfHosted() - await config.deleteConfig(Config.SETTINGS) + await config.deleteConfig(ConfigType.SETTINGS) const settingsConfig = await saveSettingsConfig() settingsConfig.config.company = "acme" settingsConfig.config.logoUrl = "http://example.com" @@ -262,14 +261,43 @@ describe("configs", () => { }) }) - it("should return the correct checklist status based on the state of the budibase installation", async () => { - await config.saveSmtpConfig() + describe("GET /api/global/configs/checklist", () => { + it("should return the correct checklist", async () => { + 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() + }) + }) + + describe("GET /api/global/configs/public", () => { + it("should return the expected public settings", async () => { + await saveSettingsConfig() + + const res = await config.api.configs.getPublicSettings() + const body = res.body as GetPublicSettingsResponse + + const expected = { + _id: "config_settings", + type: "settings", + config: { + company: "Budibase", + logoUrl: "", + analyticsEnabled: false, + google: true, + googleCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/google/callback`, + isSSOEnforced: false, + oidc: false, + oidcCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/oidc/callback`, + platformUrl: "http://localhost:10000", + }, + } + delete body._rev + expect(body).toEqual(expected) + }) }) }) diff --git a/packages/worker/src/api/routes/global/tests/realEmail.spec.ts b/packages/worker/src/api/routes/global/tests/realEmail.spec.ts index 1c180be75d..8ad7363c85 100644 --- a/packages/worker/src/api/routes/global/tests/realEmail.spec.ts +++ b/packages/worker/src/api/routes/global/tests/realEmail.spec.ts @@ -1,3 +1,4 @@ +jest.unmock("node-fetch") import { TestConfiguration } from "../../../../tests" import { EmailTemplatePurpose } from "../../../../constants" const nodemailer = require("nodemailer") diff --git a/packages/worker/src/environment.ts b/packages/worker/src/environment.ts index 71fd89f276..cd5360f7f7 100644 --- a/packages/worker/src/environment.ts +++ b/packages/worker/src/environment.ts @@ -26,8 +26,6 @@ function parseIntSafe(number: any) { } } -const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") - const environment = { // auth MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, @@ -51,7 +49,7 @@ const environment = { CLUSTER_PORT: process.env.CLUSTER_PORT, // flags NODE_ENV: process.env.NODE_ENV, - SELF_HOSTED: selfHosted, + SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), LOG_LEVEL: process.env.LOG_LEVEL, MULTI_TENANCY: process.env.MULTI_TENANCY, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, @@ -71,14 +69,6 @@ const environment = { * 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 ec6a44cc3a..04413e8429 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -13,8 +13,15 @@ import { Event } from "@sentry/types/dist/event" import Application from "koa" import { bootstrap } from "global-agent" import * as db from "./db" -import { auth, logging, events, middleware, queue } from "@budibase/backend-core" import { sdk as proSdk } from "@budibase/pro" +import { + auth, + logging, + events, + middleware, + queue, + env as coreEnv, +} from "@budibase/backend-core" db.init() import Koa from "koa" import koaBody from "koa-body" @@ -32,7 +39,7 @@ import destroyable from "server-destroy" // can't integrate directly into backend-core due to cyclic issues events.processors.init(proSdk.auditLogs.write) -if (env.ENABLE_SSO_MAINTENANCE_MODE) { +if (coreEnv.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" ) diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 15a4f3c7e7..8e9cff18dd 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -58,8 +58,8 @@ export const reset = async (email: string) => { } // exit if user has sso - if (await userSdk.isPreventSSOPasswords(user)) { - throw new HTTPError("SSO user cannot reset password", 400) + if (await userSdk.isPreventPasswordActions(user)) { + throw new HTTPError("Password reset is disabled for this user", 400) } // send password reset diff --git a/packages/worker/src/sdk/users/tests/users.spec.ts b/packages/worker/src/sdk/users/tests/users.spec.ts index 41d9298997..77f02eec7a 100644 --- a/packages/worker/src/sdk/users/tests/users.spec.ts +++ b/packages/worker/src/sdk/users/tests/users.spec.ts @@ -1,26 +1,50 @@ import { structures } from "../../../tests" -import * as users from "../users" -import env from "../../../environment" import { mocks } from "@budibase/backend-core/tests" +import { env } from "@budibase/backend-core" +import * as users from "../users" import { CloudAccount } from "@budibase/types" +import { isPreventPasswordActions } from "../users" + +jest.mock("@budibase/pro") +import * as _pro from "@budibase/pro" +const pro = jest.mocked(_pro, true) describe("users", () => { - describe("isPreventSSOPasswords", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("isPreventPasswordActions", () => { + it("returns false for non sso user", async () => { + const user = structures.users.user() + const result = await users.isPreventPasswordActions(user) + expect(result).toBe(false) + }) + 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) + const result = await users.isPreventPasswordActions(user) expect(result).toBe(true) }) it("returns true for sso user", async () => { const user = structures.users.ssoUser() - const result = await users.isPreventSSOPasswords(user) + const result = await users.isPreventPasswordActions(user) expect(result).toBe(true) }) + describe("enforced sso", () => { + it("returns true for all users when sso is enforced", async () => { + const user = structures.users.user() + pro.features.isSSOEnforced.mockReturnValue(Promise.resolve(true)) + const result = await users.isPreventPasswordActions(user) + expect(result).toBe(true) + }) + }) + describe("sso maintenance mode", () => { beforeEach(() => { env._set("ENABLE_SSO_MAINTENANCE_MODE", true) @@ -33,7 +57,7 @@ describe("users", () => { describe("non-admin user", () => { it("returns true", async () => { const user = structures.users.ssoUser() - const result = await users.isPreventSSOPasswords(user) + const result = await users.isPreventPasswordActions(user) expect(result).toBe(true) }) }) @@ -43,7 +67,7 @@ describe("users", () => { const user = structures.users.ssoUser({ user: structures.users.adminUser(), }) - const result = await users.isPreventSSOPasswords(user) + const result = await users.isPreventPasswordActions(user) expect(result).toBe(false) }) }) diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index db04f9a7e9..18d5a04cda 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -14,6 +14,7 @@ import { users as usersCore, utils, ViewName, + env as coreEnv, } from "@budibase/backend-core" import { AccountMetadata, @@ -34,7 +35,7 @@ import { } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" -import { groups as groupsSdk } from "@budibase/pro" +import * as pro from "@budibase/pro" import * as accountSdk from "../accounts" const PAGE_LIMIT = 8 @@ -122,8 +123,8 @@ const buildUser = async ( let hashedPassword if (password) { - if (await isPreventSSOPasswords(user)) { - throw new HTTPError("SSO user cannot set password", 400) + if (await isPreventPasswordActions(user)) { + throw new HTTPError("Password change is disabled for this user", 400) } hashedPassword = opts.hashPassword ? await utils.hash(password) : password } else if (dbUser) { @@ -188,13 +189,18 @@ const validateUniqueUser = async (email: string, tenantId: string) => { } } -export async function isPreventSSOPasswords(user: User) { +export async function isPreventPasswordActions(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) { + if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) { return false } + // SSO is enforced for all users + if (await pro.features.isSSOEnforced()) { + return true + } + // Check local sso if (isSSOUser(user)) { return true @@ -278,7 +284,7 @@ export const save = async ( if (userGroups.length > 0) { for (let groupId of userGroups) { - groupPromises.push(groupsSdk.addUsers(groupId, [_id])) + groupPromises.push(pro.groups.addUsers(groupId, [_id])) } } } @@ -456,7 +462,7 @@ export const bulkCreate = async ( const groupPromises = [] const createdUserIds = saved.map(user => user._id) for (let groupId of groups) { - groupPromises.push(groupsSdk.addUsers(groupId, createdUserIds)) + groupPromises.push(pro.groups.addUsers(groupId, createdUserIds)) } await Promise.all(groupPromises) } diff --git a/packages/worker/src/tests/api/configs.ts b/packages/worker/src/tests/api/configs.ts index 76a6f31415..74cef2bf8b 100644 --- a/packages/worker/src/tests/api/configs.ts +++ b/packages/worker/src/tests/api/configs.ts @@ -14,6 +14,14 @@ export class ConfigAPI extends TestAPI { .expect("Content-Type", /json/) } + getPublicSettings = () => { + return this.request + .get(`/api/global/configs/public`) + .set(this.config.defaultHeaders()) + .expect(200) + .expect("Content-Type", /json/) + } + saveConfig = (data: any) => { return this.request .post(`/api/global/configs`) diff --git a/packages/worker/src/tests/api/email.ts b/packages/worker/src/tests/api/email.ts index ba7c7dbec0..fd3c622cfa 100644 --- a/packages/worker/src/tests/api/email.ts +++ b/packages/worker/src/tests/api/email.ts @@ -13,6 +13,7 @@ export class EmailAPI extends TestAPI { email: "test@test.com", purpose, tenantId: this.config.getTenantId(), + userId: this.config.user?._id!, }) .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) diff --git a/packages/worker/src/tests/structures/configs.ts b/packages/worker/src/tests/structures/configs.ts index 2c76f271c4..d50f5ebc72 100644 --- a/packages/worker/src/tests/structures/configs.ts +++ b/packages/worker/src/tests/structures/configs.ts @@ -1,9 +1,15 @@ -import { Config } from "../../constants" import { utils } from "@budibase/backend-core" +import { + SettingsConfig, + ConfigType, + SMTPConfig, + GoogleConfig, + OIDCConfig, +} from "@budibase/types" -export function oidc(conf?: any) { +export function oidc(conf?: any): OIDCConfig { return { - type: Config.OIDC, + type: ConfigType.OIDC, config: { configs: [ { @@ -21,9 +27,9 @@ export function oidc(conf?: any) { } } -export function google(conf?: any) { +export function google(conf?: any): GoogleConfig { return { - type: Config.GOOGLE, + type: ConfigType.GOOGLE, config: { clientID: "clientId", clientSecret: "clientSecret", @@ -33,9 +39,9 @@ export function google(conf?: any) { } } -export function smtp(conf?: any) { +export function smtp(conf?: any): SMTPConfig { return { - type: Config.SMTP, + type: ConfigType.SMTP, config: { port: 12345, host: "smtptesthost.com", @@ -47,25 +53,26 @@ export function smtp(conf?: any) { } } -export function smtpEthereal() { +export function smtpEthereal(): SMTPConfig { return { - type: Config.SMTP, + type: ConfigType.SMTP, config: { port: 587, host: "smtp.ethereal.email", + from: "testfrom@test.com", secure: false, auth: { - user: "don.bahringer@ethereal.email", - pass: "yCKSH8rWyUPbnhGYk9", + user: "wyatt.zulauf29@ethereal.email", + pass: "tEwDtHBWWxusVWAPfa", }, connectionTimeout: 1000, // must be less than the jest default of 5000 }, } } -export function settings(conf?: any) { +export function settings(conf?: any): SettingsConfig { return { - type: Config.SETTINGS, + type: ConfigType.SETTINGS, config: { platformUrl: "http://localhost:10000", logoUrl: "", diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index 66e860edcb..69861684eb 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -1,11 +1,11 @@ import env from "../environment" -import { EmailTemplatePurpose, TemplateType, Config } from "../constants" +import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" import { getResetPasswordCode, getInviteCode } from "./redis" -import { User, Database } from "@budibase/types" -import { tenancy, db as dbCore } from "@budibase/backend-core" +import { User, SMTPInnerConfig } from "@budibase/types" +import { configs } from "@budibase/backend-core" const nodemailer = require("nodemailer") type SendEmailOpts = { @@ -36,24 +36,24 @@ const FULL_EMAIL_PURPOSES = [ EmailTemplatePurpose.CUSTOM, ] -function createSMTPTransport(config: any) { +function createSMTPTransport(config?: SMTPInnerConfig) { let options: any - let secure = config.secure + let secure = config?.secure // default it if not specified if (secure == null) { - secure = config.port === 465 + secure = config?.port === 465 } if (!TEST_MODE) { options = { - port: config.port, - host: config.host, + port: config?.port, + host: config?.host, secure: secure, - auth: config.auth, + auth: config?.auth, } options.tls = { rejectUnauthorized: false, } - if (config.connectionTimeout) { + if (config?.connectionTimeout) { options.connectionTimeout = config.connectionTimeout } } else { @@ -134,57 +134,16 @@ async function buildEmail( }) } -/** - * Utility function for finding most valid SMTP configuration. - * @param {object} db The CouchDB database which is to be looked up within. - * @param {string|null} workspaceId If using finer grain control of configs a workspace can be used. - * @param {boolean|null} automation Whether or not the configuration is being fetched for an email automation. - * @return {Promise} returns the SMTP configuration if it exists - */ -async function getSmtpConfiguration( - db: Database, - workspaceId?: string, - automation?: boolean -) { - const params: any = { - type: Config.SMTP, - } - if (workspaceId) { - params.workspace = workspaceId - } - - const customConfig = await dbCore.getScopedConfig(db, params) - - if (customConfig) { - return customConfig - } - - // Use an SMTP fallback configuration from env variables - if (!automation && env.SMTP_FALLBACK_ENABLED) { - return { - port: env.SMTP_PORT, - host: env.SMTP_HOST, - secure: false, - from: env.SMTP_FROM_ADDRESS, - auth: { - user: env.SMTP_USER, - pass: env.SMTP_PASSWORD, - }, - } - } -} - /** * Checks if a SMTP config exists based on passed in parameters. * @return {Promise} returns true if there is a configuration that can be used. */ -export async function isEmailConfigured(workspaceId?: string) { +export async function isEmailConfigured() { // when "testing" or smtp fallback is enabled simply return true if (TEST_MODE || env.SMTP_FALLBACK_ENABLED) { return true } - const db = tenancy.getGlobalDB() - const config = await getSmtpConfiguration(db, workspaceId) + const config = await configs.getSMTPConfig() return config != null } @@ -202,22 +161,17 @@ export async function sendEmail( purpose: EmailTemplatePurpose, opts: SendEmailOpts ) { - const db = tenancy.getGlobalDB() - let config = - (await getSmtpConfiguration(db, opts?.workspaceId, opts?.automation)) || {} - if (Object.keys(config).length === 0 && !TEST_MODE) { + const config = await configs.getSMTPConfig(opts?.automation) + if (!config && !TEST_MODE) { throw "Unable to find SMTP configuration." } const transport = createSMTPTransport(config) // if there is a link code needed this will retrieve it const code = await getLinkCode(purpose, email, opts.user, opts?.info) - let context - if (code) { - context = await getSettingsTemplateContext(purpose, code) - } + let context = await getSettingsTemplateContext(purpose, code) let message: any = { - from: opts?.from || config.from, + from: opts?.from || config?.from, html: await buildEmail(purpose, email, context, { user: opts?.user, contents: opts?.contents, @@ -231,9 +185,9 @@ export async function sendEmail( bcc: opts?.bcc, } - if (opts?.subject || config.subject) { + if (opts?.subject || config?.subject) { message.subject = await processString( - opts?.subject || config.subject, + (opts?.subject || config?.subject) as string, context ) } diff --git a/packages/worker/src/utilities/templates.ts b/packages/worker/src/utilities/templates.ts index ede95dbe4a..1597325c7c 100644 --- a/packages/worker/src/utilities/templates.ts +++ b/packages/worker/src/utilities/templates.ts @@ -1,6 +1,5 @@ -import { db as dbCore, tenancy } from "@budibase/backend-core" +import { tenancy, configs } from "@budibase/backend-core" import { - Config, InternalTemplateBinding, LOGO_URL, EmailTemplatePurpose, @@ -10,20 +9,16 @@ const BASE_COMPANY = "Budibase" export async function getSettingsTemplateContext( purpose: EmailTemplatePurpose, - code?: string + code?: string | null ) { - const db = tenancy.getGlobalDB() - // TODO: use more granular settings in the future if required - let settings = - (await dbCore.getScopedConfig(db, { type: Config.SETTINGS })) || {} + let settings = await configs.getSettingsConfig() const URL = settings.platformUrl const context: any = { [InternalTemplateBinding.LOGO_URL]: checkSlashesInUrl(`${URL}/${settings.logoUrl}`) || LOGO_URL, [InternalTemplateBinding.PLATFORM_URL]: URL, [InternalTemplateBinding.COMPANY]: settings.company || BASE_COMPANY, - [InternalTemplateBinding.DOCS_URL]: - settings.docsUrl || "https://docs.budibase.com/", + [InternalTemplateBinding.DOCS_URL]: "https://docs.budibase.com/", [InternalTemplateBinding.LOGIN_URL]: checkSlashesInUrl( tenancy.addTenantToUrl(`${URL}/login`) ), diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index b781b85982..fb94dfa99b 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.18-alpha.12": - version "2.3.18-alpha.12" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.12.tgz#ad1b16be64b78b596af2b5f75647c32e8f6f101a" - integrity sha512-E1NEO+/sNkkRqn/xk9XQmFBO9/dl27w9EB0QGztti/16JV9NgxyDQCJIdGwlD08s1y/lUwOKk0TkSZJs+CTYDw== +"@budibase/backend-core@2.3.18-alpha.13": + version "2.3.18-alpha.13" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.13.tgz#b797d7a4d30ff7f21e473334edb4086f5818830e" + integrity sha512-c6d0xCRgLlPeX1euAoQuoDOkMkDGQy/miBx/Z8xyU9bzDDTNqBTogxpxsNf8DdZG7EMJhsJlCUvb26Onz7/50A== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.12" + "@budibase/types" "2.3.18-alpha.13" "@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.18-alpha.12": - version "2.3.18-alpha.12" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.12.tgz#be552b3a9f5850e746081540d6586aae69147bec" - integrity sha512-M3b0njzSi47KH6uaQfYPoA2KWrjPiwcU3ONyaVWXHIktVrIKtYaFwOLBr/dmWGfMrL2297SSqg7V4DTaLyAhnw== +"@budibase/pro@2.3.18-alpha.13": + version "2.3.18-alpha.13" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.13.tgz#9f3dd0339184a58609c726f3f2b4580cf802dc78" + integrity sha512-pRroVVFGITFsFtzzH6LgzuaUBKldvFQxTgO7K6dYjjE7xwTCnZCg0E4L2Ew/JIY39UN2WEb5bwdt3+pqPH7Dmg== dependencies: - "@budibase/backend-core" "2.3.18-alpha.12" - "@budibase/types" "2.3.18-alpha.12" + "@budibase/backend-core" "2.3.18-alpha.13" + "@budibase/types" "2.3.18-alpha.13" "@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.18-alpha.12": - version "2.3.18-alpha.12" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.12.tgz#a63eb978ccc7e55c209b3e9d71f9aecf7facc0d1" - integrity sha512-27o2BmI/HXIR3frZ8FtqHgAe1hd8jPIzgPaEhKrQiYJ/opUVccqupx9ld75Hyk9E6cdXu0UF0/+LxPpUmMugag== +"@budibase/types@2.3.18-alpha.13": + version "2.3.18-alpha.13" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.13.tgz#e80cbf79249ed5cf8fe86e294d0b1e25e0a839ee" + integrity sha512-fmgpwMMGkbPOObmFnZZH8iKelycmhvBydGQuPEmIk1c449ysfiKF2Strnca6MaY1XtskfRz/nNdWGdGS1HhmFw== "@cspotcode/source-map-support@^0.8.0": version "0.8.1"