From d3a7286711be145f52555c8e255f4ce818df01ef Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 23 Feb 2023 13:41:35 +0000 Subject: [PATCH] Configs updates: remove circular deps, dedicated module, typing improvements, reduce db reads --- packages/backend-core/src/auth/auth.ts | 101 ++++---- packages/backend-core/src/configs/configs.ts | 224 ++++++++++++++++++ packages/backend-core/src/configs/index.ts | 1 + .../src/configs/tests/configs.spec.ts | 116 +++++++++ .../backend-core/src/db/tests/utils.spec.ts | 133 +---------- packages/backend-core/src/db/utils.ts | 139 +---------- packages/backend-core/src/environment.ts | 7 + packages/backend-core/src/events/analytics.ts | 55 +---- .../backend-core/src/events/identification.ts | 7 +- packages/backend-core/src/index.ts | 1 + .../src/middleware/authenticated.ts | 3 +- .../src/middleware/errorHandling.ts | 1 + .../middleware/passport/datasource/google.ts | 28 +-- .../src/middleware/passport/sso/google.ts | 12 +- .../src/middleware/passport/sso/oidc.ts | 8 +- .../src/middleware/passport/utils.ts | 21 +- .../tests/utilities/structures/sso.ts | 1 + .../server/src/integrations/googlesheets.ts | 14 +- .../functions/backfill/global/configs.ts | 24 +- .../server/src/migrations/tests/helpers.ts | 4 +- .../server/src/migrations/tests/structures.ts | 1 + packages/types/src/api/web/global/configs.ts | 23 ++ packages/types/src/api/web/global/index.ts | 1 + packages/types/src/documents/global/config.ts | 43 ++-- .../worker/src/api/controllers/global/auth.ts | 69 +++--- .../src/api/controllers/global/configs.ts | 219 ++++++----------- .../worker/src/api/routes/global/configs.ts | 6 - 27 files changed, 615 insertions(+), 647 deletions(-) create mode 100644 packages/backend-core/src/configs/configs.ts create mode 100644 packages/backend-core/src/configs/index.ts create mode 100644 packages/backend-core/src/configs/tests/configs.spec.ts create mode 100644 packages/types/src/api/web/global/configs.ts diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index bee245a3ae..435b6433b2 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -2,25 +2,33 @@ 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, + 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 +41,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 +70,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 +96,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 +106,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 +129,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 +138,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") + configType: ConfigType, + configId?: string +): Promise { + if (configType === ConfigType.OIDC && configId) { + const config = await configs.getOIDCConfigById(configId) + if (!config) { + return { err: { data: "OIDC configuration not found" } } } - refreshResponse = await refreshOIDCAccessToken( - db, - chosenConfig, - refreshToken - ) - } else { - chosenConfig = config - refreshResponse = await refreshGoogleAccessToken( - db, - chosenConfig, - refreshToken - ) + return refreshOIDCAccessToken(config, refreshToken) } - return refreshResponse + if (configType === ConfigType.GOOGLE) { + const config = await configs.getGoogleConfig() + if (!config) { + return { err: { data: "Google configuration not found" } } + } + return refreshGoogleAccessToken(config, refreshToken) + } + + throw new Error(`Unsupported configType=${configType}`) } // 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 233d044eaa..948b59b13a 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. @@ -392,32 +391,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. @@ -441,109 +414,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, @@ -577,8 +447,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 ed7a161160..d565606133 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -84,6 +84,13 @@ const environment = { DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true, + // 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, _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 7cade9e14b..75cd4ae7ec 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -19,10 +19,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") @@ -270,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 d507d8175f..601b14a661 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 4bb2aaba76..23fb87df8b 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/structures/sso.ts b/packages/backend-core/tests/utilities/structures/sso.ts index ad5e8e87ef..a63d26cbb3 100644 --- a/packages/backend-core/tests/utilities/structures/sso.ts +++ b/packages/backend-core/tests/utilities/structures/sso.ts @@ -69,6 +69,7 @@ export function oidcConfig(): OIDCInnerConfig { configUrl: "http://someconfigurl", clientID: generator.string(), clientSecret: generator.string(), + scopes: [], } } 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/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/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 415ed55ab1..d6e87d3a0d 100644 --- a/packages/types/src/api/web/global/index.ts +++ b/packages/types/src/api/web/global/index.ts @@ -1,2 +1,3 @@ export * from "./environmentVariables" 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..8c423feb65 100644 --- a/packages/types/src/documents/global/config.ts +++ b/packages/types/src/documents/global/config.ts @@ -5,32 +5,42 @@ 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 } 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 +65,7 @@ export interface OIDCInnerConfig { name: string uuid: string activated: boolean + scopes: string[] } export interface OIDCConfig extends Config { diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 948a98cf3a..ab017a4184 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" @@ -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..b076416989 100644 --- a/packages/worker/src/api/controllers/global/configs.ts +++ b/packages/worker/src/api/controllers/global/configs.ts @@ -2,38 +2,31 @@ 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" -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 +118,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) +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) + // Config does not exist yet - if (!ctx.request.body._id) { - ctx.request.body._id = dbCore.generateConfigID({ - type, - workspace, - user, - }) + if (!existingConfig) { + body._id = configs.generateConfigID(type) } try { // verify the configuration - switch (type) { + switch (config.type) { case ConfigType.SMTP: await email.verifyConfig(config) break @@ -149,7 +142,7 @@ export async function save(ctx: UserCtx) { } try { - const response = await db.put(ctx.request.body) + const response = await configs.save(body) await cache.bustCache(cache.CacheKey.CHECKLIST) await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED) @@ -167,44 +160,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 +177,64 @@ 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 config = await configs.getSettingsConfig() + // 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() + + ctx.body = { + type: ConfigType.SETTINGS, + _id: configs.generateConfigID(ConfigType.SETTINGS), + config: { + ...config, + google, + oidc, + 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 +258,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 +270,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 +298,6 @@ export async function destroy(ctx: UserCtx) { } export async function configChecklist(ctx: Ctx) { - const db = tenancy.getGlobalDB() const tenantId = tenancy.getTenantId() try { @@ -375,19 +312,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)