budibase/packages/worker/src/api/controllers/global/configs.ts

469 lines
12 KiB
TypeScript
Raw Normal View History

import * as email from "../../../utilities/email"
import env from "../../../environment"
import { googleCallbackUrl, oidcCallbackUrl } from "./auth"
import {
2022-06-29 14:08:48 +02:00
cache,
configs,
db as dbCore,
env as coreEnv,
events,
objectStore,
tenancy,
} from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users"
import {
Config,
ConfigType,
Ctx,
2023-03-27 18:28:12 +02:00
Feature,
GetPublicOIDCConfigResponse,
GetPublicSettingsResponse,
GoogleInnerConfig,
isGoogleConfig,
isOIDCConfig,
isSettingsConfig,
isSMTPConfig,
OIDCConfigs,
SettingsInnerConfig,
SSOConfig,
SSOConfigType,
UserCtx,
} from "@budibase/types"
import * as pro from "@budibase/pro"
2023-03-27 18:28:12 +02:00
import { licensing } from "@budibase/pro"
const getEventFns = async (config: Config, existing?: Config) => {
const fns = []
2022-04-05 17:56:28 +02:00
if (!existing) {
if (isSMTPConfig(config)) {
fns.push(events.email.SMTPCreated)
} else if (isGoogleConfig(config)) {
fns.push(() => events.auth.SSOCreated(ConfigType.GOOGLE))
if (config.config.activated) {
fns.push(() => events.auth.SSOActivated(ConfigType.GOOGLE))
}
} else if (isOIDCConfig(config)) {
fns.push(() => events.auth.SSOCreated(ConfigType.OIDC))
if (config.config.configs[0].activated) {
fns.push(() => events.auth.SSOActivated(ConfigType.OIDC))
}
} else if (isSettingsConfig(config)) {
// company
const company = config.config.company
if (company && company !== "Budibase") {
fns.push(events.org.nameUpdated)
}
// logo
const logoUrl = config.config.logoUrl
if (logoUrl) {
fns.push(events.org.logoUpdated)
}
// platform url
const platformUrl = config.config.platformUrl
if (
platformUrl &&
platformUrl !== "http://localhost:10000" &&
env.SELF_HOSTED
) {
fns.push(events.org.platformURLUpdated)
}
}
} else {
if (isSMTPConfig(config)) {
fns.push(events.email.SMTPUpdated)
} else if (isGoogleConfig(config)) {
fns.push(() => events.auth.SSOUpdated(ConfigType.GOOGLE))
if (!existing.config.activated && config.config.activated) {
fns.push(() => events.auth.SSOActivated(ConfigType.GOOGLE))
} else if (existing.config.activated && !config.config.activated) {
fns.push(() => events.auth.SSODeactivated(ConfigType.GOOGLE))
}
} else if (isOIDCConfig(config)) {
fns.push(() => events.auth.SSOUpdated(ConfigType.OIDC))
if (
!existing.config.configs[0].activated &&
config.config.configs[0].activated
) {
fns.push(() => events.auth.SSOActivated(ConfigType.OIDC))
} else if (
existing.config.configs[0].activated &&
!config.config.configs[0].activated
) {
fns.push(() => events.auth.SSODeactivated(ConfigType.OIDC))
}
} else if (isSettingsConfig(config)) {
// company
const existingCompany = existing.config.company
const company = config.config.company
if (company && company !== "Budibase" && existingCompany !== company) {
fns.push(events.org.nameUpdated)
}
// logo
const existingLogoUrl = existing.config.logoUrl
const logoUrl = config.config.logoUrl
if (logoUrl && existingLogoUrl !== logoUrl) {
fns.push(events.org.logoUpdated)
}
// platform url
const existingPlatformUrl = existing.config.platformUrl
const platformUrl = config.config.platformUrl
if (
platformUrl &&
platformUrl !== "http://localhost:10000" &&
existingPlatformUrl !== platformUrl &&
env.SELF_HOSTED
) {
fns.push(events.org.platformURLUpdated)
}
}
}
return fns
}
2022-04-05 17:56:28 +02:00
type SSOConfigs = { [key in SSOConfigType]: SSOConfig | undefined }
async function getSSOConfigs(): Promise<SSOConfigs> {
const google = await configs.getGoogleConfig()
const oidc = await configs.getOIDCConfig()
return {
[ConfigType.GOOGLE]: google,
[ConfigType.OIDC]: oidc,
}
}
async function hasActivatedConfig(ssoConfigs?: SSOConfigs) {
if (!ssoConfigs) {
ssoConfigs = await getSSOConfigs()
}
2023-02-28 14:37:34 +01:00
return !!Object.values(ssoConfigs).find(c => c?.activated)
}
async function verifySettingsConfig(config: SettingsInnerConfig) {
if (config.isSSOEnforced) {
const valid = await hasActivatedConfig()
if (!valid) {
2023-02-28 14:37:34 +01:00
throw new Error("Cannot enforce SSO without an activated configuration")
}
}
}
async function verifySSOConfig(type: SSOConfigType, config: SSOConfig) {
const settings = await configs.getSettingsConfig()
if (settings.isSSOEnforced && !config.activated) {
// config is being saved as deactivated
// ensure there is at least one other activated sso config
const ssoConfigs = await getSSOConfigs()
// overwrite the config being updated
// to reflect the desired state
ssoConfigs[type] = config
const activated = await hasActivatedConfig(ssoConfigs)
if (!activated) {
throw new Error(
"Configuration cannot be deactivated while SSO is enforced"
)
}
}
}
async function verifyGoogleConfig(config: GoogleInnerConfig) {
await verifySSOConfig(ConfigType.GOOGLE, config)
}
async function verifyOIDCConfig(config: OIDCConfigs) {
await verifySSOConfig(ConfigType.OIDC, config.configs[0])
}
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)
if (existingConfig) {
body._rev = existingConfig._rev
2021-04-20 19:14:36 +02:00
}
try {
// verify the configuration
switch (type) {
case ConfigType.SMTP:
await email.verifyConfig(config)
break
case ConfigType.SETTINGS:
await verifySettingsConfig(config)
break
case ConfigType.GOOGLE:
await verifyGoogleConfig(config)
break
case ConfigType.OIDC:
await verifyOIDCConfig(config)
break
}
} catch (err: any) {
ctx.throw(400, err)
}
2021-04-20 19:14:36 +02:00
try {
body._id = configs.generateConfigID(type)
const response = await configs.save(body)
await cache.bustCache(cache.CacheKey.CHECKLIST)
await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED)
for (const fn of eventFns) {
2022-05-23 23:14:44 +02:00
await fn()
}
2021-04-20 19:14:36 +02:00
ctx.body = {
2021-04-22 12:45:22 +02:00
type,
2021-04-20 19:14:36 +02:00
_id: response.id,
_rev: response.rev,
}
} catch (err: any) {
ctx.throw(400, err)
2021-04-20 19:14:36 +02:00
}
}
export async function find(ctx: UserCtx) {
2021-04-20 19:14:36 +02:00
try {
2021-04-22 12:45:22 +02:00
// Find the config with the most granular scope based on context
const type = ctx.params.type
const scopedConfig = await configs.getConfig(type)
2021-04-22 12:45:22 +02:00
if (scopedConfig) {
ctx.body = scopedConfig
} else {
// don't throw an error, there simply is nothing to return
ctx.body = {}
2021-04-22 12:45:22 +02:00
}
} catch (err: any) {
ctx.throw(err?.status || 400, err)
2021-04-20 19:14:36 +02:00
}
}
export async function publicOidc(ctx: Ctx<void, GetPublicOIDCConfigResponse>) {
2021-07-13 15:54:20 +02:00
try {
// Find the config with the most granular scope based on context
const config = await configs.getOIDCConfig()
2021-07-13 15:54:20 +02:00
if (!config) {
ctx.body = []
2021-07-13 15:54:20 +02:00
} else {
ctx.body = [
{
logo: config.logo,
name: config.name,
uuid: config.uuid,
},
]
2021-07-13 15:54:20 +02:00
}
} catch (err: any) {
2021-07-13 15:54:20 +02:00
ctx.throw(err.status, err)
}
}
2023-03-27 19:00:57 +02:00
export async function getLicensedConfig() {
let licensedConfig: object = {}
const defaults = {
emailBrandingEnabled: true,
testimonialsEnabled: true,
platformTitle: undefined,
metaDescription: undefined,
metaImageUrl: undefined,
metaTitle: undefined,
}
try {
// License/Feature Checks
const license = await licensing.getLicense()
if (!license || license?.features.indexOf(Feature.BRANDING) == -1) {
licensedConfig = { ...defaults }
}
} catch (e) {
licensedConfig = { ...defaults }
console.info("Could not retrieve license", e)
}
return licensedConfig
}
export async function publicSettings(
ctx: Ctx<void, GetPublicSettingsResponse>
) {
try {
// settings
const configDoc = await configs.getSettingsConfigDoc()
const config = configDoc.config
2023-03-27 18:28:12 +02:00
2023-03-27 19:00:57 +02:00
const licensedConfig: object = await getLicensedConfig()
2023-03-27 18:28:12 +02:00
// enrich the logo url - empty url means deleted
if (config.logoUrl && config.logoUrl !== "") {
config.logoUrl = objectStore.getGlobalFileUrl(
"settings",
"logoUrl",
config.logoUrlEtag
)
}
2023-03-13 13:33:16 +01:00
if (config.faviconUrl && config.faviconUrl !== "") {
config.faviconUrl = objectStore.getGlobalFileUrl(
"settings",
"faviconUrl",
config.faviconUrl
)
}
// google
const googleConfig = await configs.getGoogleConfig()
const preActivated = googleConfig && googleConfig.activated == null
const google = preActivated || !!googleConfig?.activated
const _googleCallbackUrl = await googleCallbackUrl(googleConfig)
// oidc
const oidcConfig = await configs.getOIDCConfig()
const oidc = oidcConfig?.activated || false
const _oidcCallbackUrl = await oidcCallbackUrl()
// sso enforced
const isSSOEnforced = await pro.features.isSSOEnforced({ config })
ctx.body = {
type: ConfigType.SETTINGS,
_id: configDoc._id,
_rev: configDoc._rev,
config: {
...config,
2023-03-27 18:28:12 +02:00
...licensedConfig,
google,
oidc,
isSSOEnforced,
oidcCallbackUrl: _oidcCallbackUrl,
googleCallbackUrl: _googleCallbackUrl,
},
}
} catch (err: any) {
ctx.throw(err.status, err)
}
}
export async function upload(ctx: UserCtx) {
if (ctx.request.files == null || Array.isArray(ctx.request.files.file)) {
ctx.throw(400, "One file must be uploaded.")
}
fixes for google sheets, admin checklist, and deleting an app from API (#8846) * fixes for google sheets, admin checklist, and deleting an app from API * code review * splitting unpublish endpoint, moving deploy endpoint to applications controller. Still to do public API work and move deployment controller into application controller * updating REST method for unpublish in API test * unpublish and publish endpoint on public API, delete endpoint unpublishes and deletes app * removing skip_setup from prodAppDb call * removing commented code * unit tests and open API spec updates * unpublish, publish unit tests - delete still in progress * remove line updating app name in API test * unit tests * v2.1.46 * Update pro version to 2.1.46 * v2.2.0 * Update pro version to 2.2.0 * Fix for budibase plugin skeleton, which utilises the old import style. * Fix side nav styles * v2.2.1 * Update pro version to 2.2.1 * using dist folder to allow importing constants for openAPI specs * v2.2.2 * Update pro version to 2.2.2 * Fix for user enrichment call (updating to @budibase/nano fork) (#9038) * Fix for #9029 - this should fix the issue users have been experiencing with user enrichment calls in apps, essentially it utilises a fork of the nano library we use to interact with CouchDB, which has been updated to use a POST request rather than a GET request as it supports a larger set of data being sent as query parameters. * Incrementing Nano version to attempt to fix yarn registry issues. * v2.2.3 * Update pro version to 2.2.3 * Fix SQL table `_id` filtering (#9030) * Re-add support for filtering on _id using external SQL tables and fix filter key prefixes not working with _id field * Remove like operator from internal tables and only allow basic operators on SQL table _id column * Update data section filtering to respect new rules * Update automation section filtering to respect new rules * Update dynamic filter component to respect new rules * v2.2.4 * Update pro version to 2.2.4 * lock changes (#9047) * v2.2.5 * Update pro version to 2.2.5 * Make looping arrow point in right direction (#9053) * v2.2.6 * Update pro version to 2.2.6 * Types/attaching license to account (#9065) * adding license type to account * removing planDuration * v2.2.7 * Update pro version to 2.2.7 * Environment variable type coercion fix (#9074) * Environment variable type coercion fix * Update .gitignore * v2.2.8 * Update pro version to 2.2.8 * tests passing * all tests passing, updates to public API response * update unpublish call to return 204, openAPI spec and unit * fixing API tests Co-authored-by: Budibase Release Bot <> Co-authored-by: mike12345567 <me@michaeldrury.co.uk> Co-authored-by: Andrew Kingston <andrew@kingston.dev> Co-authored-by: melohagan <101575380+melohagan@users.noreply.github.com> Co-authored-by: Rory Powell <rory.codes@gmail.com>
2022-12-19 14:18:00 +01:00
const file = ctx.request.files.file as any
const { type, name } = ctx.params
let bucket = coreEnv.GLOBAL_BUCKET_NAME
const key = objectStore.getGlobalFileS3Key(type, name)
const result = await objectStore.upload({
bucket,
filename: key,
path: file.path,
type: file.type,
})
// add to configuration structure
let config = await configs.getConfig(type)
if (!config) {
config = {
_id: configs.generateConfigID(type),
type,
config: {},
}
}
// save the Etag for cache bursting
const etag = result.ETag
if (etag) {
config.config[`${name}Etag`] = etag.replace(/"/g, "")
}
// save the file key
config.config[`${name}`] = key
// write back to db
await configs.save(config)
ctx.body = {
message: "File has been uploaded and url stored to config.",
url: objectStore.getGlobalFileUrl(type, name, etag),
}
}
export async function destroy(ctx: UserCtx) {
const db = tenancy.getGlobalDB()
2021-04-20 19:14:36 +02:00
const { id, rev } = ctx.params
try {
await db.remove(id, rev)
await cache.destroy(cache.CacheKey.CHECKLIST)
2021-04-20 19:14:36 +02:00
ctx.body = { message: "Config deleted successfully" }
} catch (err: any) {
2021-04-20 19:14:36 +02:00
ctx.throw(err.status, err)
}
}
export async function configChecklist(ctx: Ctx) {
const tenantId = tenancy.getTenantId()
try {
ctx.body = await cache.withCache(
cache.CacheKey.CHECKLIST,
env.CHECKLIST_CACHE_TTL,
async () => {
let apps = []
if (!env.MULTI_TENANCY || tenantId) {
// Apps exist
apps = await dbCore.getAllApps({ idsOnly: true, efficient: true })
}
2021-05-21 15:55:11 +02:00
// They have set up SMTP
const smtpConfig = await configs.getSMTPConfig()
// They have set up Google Auth
const googleConfig = await configs.getGoogleConfig()
// They have set up OIDC
const oidcConfig = await configs.getOIDCConfig()
// They have set up a global user
const userExists = await checkAnyUserExists()
return {
apps: {
checked: apps.length > 0,
label: "Create your first app",
link: "/builder/portal/apps",
},
smtp: {
checked: !!smtpConfig,
label: "Set up email",
link: "/builder/portal/manage/email",
},
adminUser: {
checked: userExists,
label: "Create your first user",
link: "/builder/portal/manage/users",
},
sso: {
checked: !!googleConfig || !!oidcConfig,
label: "Set up single sign-on",
link: "/builder/portal/manage/auth",
},
}
}
)
} catch (err: any) {
ctx.throw(err.status, err)
}
}