Configs updates: remove circular deps, dedicated module, typing improvements, reduce db reads
This commit is contained in:
parent
20a2346b04
commit
6da72bb2c6
|
@ -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<RefreshResponse> {
|
||||
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<RefreshResponse> {
|
||||
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<RefreshResponse> {
|
||||
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
|
||||
|
|
|
@ -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<T extends Config>(
|
||||
type: ConfigType
|
||||
): Promise<T | undefined> {
|
||||
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<SettingsConfig> {
|
||||
let config = await getConfig<SettingsConfig>(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<SettingsInnerConfig> {
|
||||
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<SettingsConfig>(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<SettingsConfig>(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<GoogleConfig | undefined> {
|
||||
return await getConfig<GoogleConfig>(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<OIDCConfig | undefined> {
|
||||
return getConfig<OIDCConfig>(ConfigType.OIDC)
|
||||
}
|
||||
|
||||
export async function getOIDCConfig(): Promise<OIDCInnerConfig | undefined> {
|
||||
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<OIDCInnerConfig | undefined> {
|
||||
const config = (await getConfig<OIDCConfig>(ConfigType.OIDC))?.config
|
||||
return config && config.configs.filter((c: any) => c.uuid === configId)[0]
|
||||
}
|
||||
|
||||
// SMTP
|
||||
|
||||
export async function getSMTPConfigDoc(): Promise<SMTPConfig | undefined> {
|
||||
return getConfig<SMTPConfig>(ConfigType.SMTP)
|
||||
}
|
||||
|
||||
export async function getSMTPConfig(
|
||||
isAutomation?: boolean
|
||||
): Promise<SMTPInnerConfig | undefined> {
|
||||
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!,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./configs"
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<string> => {
|
|||
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) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * as configs from "./configs"
|
||||
export * as events from "./events"
|
||||
export * as migrations from "./migrations"
|
||||
export * as users from "./users"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ export function oidcConfig(): OIDCInnerConfig {
|
|||
configUrl: "http://someconfigurl",
|
||||
clientID: generator.string(),
|
||||
clientSecret: generator.string(),
|
||||
scopes: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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<Config[]> => {
|
||||
const response = await globalDb.allDocs(
|
||||
dbUtils.getConfigParams(
|
||||
{},
|
||||
{
|
||||
include_docs: true,
|
||||
}
|
||||
)
|
||||
)
|
||||
const response = await globalDb.allDocs(getConfigParams())
|
||||
return response.rows.map((row: any) => row.doc)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -20,6 +20,7 @@ export const oidc = (conf?: OIDCConfig): OIDCConfig => {
|
|||
name: "Active Directory",
|
||||
uuid: utils.newid(),
|
||||
activated: true,
|
||||
scopes: [],
|
||||
...conf,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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[]
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./environmentVariables"
|
||||
export * from "./events"
|
||||
export * from "./configs"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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<Config>) {
|
||||
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<void, GetPublicOIDCConfigResponse>) {
|
||||
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<void, GetPublicSettingsResponse>
|
||||
) {
|
||||
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()
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue