diff --git a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte
index e5196a1cbf..236636ecd5 100644
--- a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte
+++ b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte
@@ -131,24 +131,25 @@
isEqual(providers.google?.config, originalGoogleDoc?.config)
? (googleSaveButtonDisabled = true)
: (googleSaveButtonDisabled = false)
+
+ // delete the callback url which is never saved to the oidc
+ // config doc, to ensure an accurate comparison
+ delete providers.oidc?.config.configs[0].callbackURL
+
isEqual(providers.oidc?.config, originalOidcDoc?.config)
? (oidcSaveButtonDisabled = true)
: (oidcSaveButtonDisabled = false)
}
- // Create a flag so that it will only try to save completed forms
- $: partialGoogle =
- providers.google?.config?.clientID || providers.google?.config?.clientSecret
- $: partialOidc =
- providers.oidc?.config?.configs[0].configUrl ||
- providers.oidc?.config?.configs[0].clientID ||
- providers.oidc?.config?.configs[0].clientSecret
- $: googleComplete =
+ $: googleComplete = !!(
providers.google?.config?.clientID && providers.google?.config?.clientSecret
- $: oidcComplete =
+ )
+
+ $: oidcComplete = !!(
providers.oidc?.config?.configs[0].configUrl &&
providers.oidc?.config?.configs[0].clientID &&
providers.oidc?.config?.configs[0].clientSecret
+ )
const onFileSelected = e => {
let fileName = e.target.files[0].name
@@ -159,74 +160,88 @@
async function toggleIsSSOEnforced() {
const value = $organisation.isSSOEnforced
- await organisation.save({ isSSOEnforced: !value })
+ try {
+ await organisation.save({ isSSOEnforced: !value })
+ } catch (e) {
+ notifications.error(e.message)
+ }
}
- async function save(docs) {
- let calls = []
- // Only if the user has provided an image, upload it
+ async function saveConfig(config) {
+ // Delete unsupported fields
+ delete config.createdAt
+ delete config.updatedAt
+ return API.saveConfig(config)
+ }
+
+ async function saveOIDCLogo() {
if (image) {
let data = new FormData()
data.append("file", image)
- calls.push(
- API.uploadOIDCLogo({
- name: image.name,
- data,
- })
+ await API.uploadOIDCLogo({
+ name: image.name,
+ data,
+ })
+ }
+ }
+
+ async function saveOIDC() {
+ if (!oidcComplete) {
+ notifications.error(
+ `Please fill in all required ${ConfigTypes.OIDC} fields`
)
+ return
}
- docs.forEach(element => {
- // Delete unsupported fields
- delete element.createdAt
- delete element.updatedAt
- const { activated } = element.config
+ const oidc = providers.oidc
- if (element.type === ConfigTypes.OIDC) {
- // Add a UUID here so each config is distinguishable when it arrives at the login page
- for (let config of element.config.configs) {
- if (!config.uuid) {
- config.uuid = Helpers.uuid()
- }
- // Callback urls shouldn't be included
- delete config.callbackURL
- }
- if ((partialOidc || activated) && !oidcComplete) {
- notifications.error(
- `Please fill in all required ${ConfigTypes.OIDC} fields`
- )
- } else if (oidcComplete || !activated) {
- calls.push(API.saveConfig(element))
- // Turn the save button grey when clicked
- oidcSaveButtonDisabled = true
- originalOidcDoc = cloneDeep(providers.oidc)
- }
+ // Add a UUID here so each config is distinguishable when it arrives at the login page
+ for (let config of oidc.config.configs) {
+ if (!config.uuid) {
+ config.uuid = Helpers.uuid()
}
- if (element.type === ConfigTypes.Google) {
- if ((partialGoogle || activated) && !googleComplete) {
- notifications.error(
- `Please fill in all required ${ConfigTypes.Google} fields`
- )
- } else if (googleComplete || !activated) {
- calls.push(API.saveConfig(element))
- googleSaveButtonDisabled = true
- originalGoogleDoc = cloneDeep(providers.google)
- }
- }
- })
- if (calls.length) {
- Promise.all(calls)
- .then(data => {
- data.forEach(res => {
- providers[res.type]._rev = res._rev
- providers[res.type]._id = res._id
- })
- notifications.success(`Settings saved`)
- })
- .catch(() => {
- notifications.error("Failed to update auth settings")
- })
+ // Callback urls shouldn't be included
+ delete config.callbackURL
}
+
+ try {
+ const res = await saveConfig(oidc)
+ providers[res.type]._rev = res._rev
+ providers[res.type]._id = res._id
+ await saveOIDCLogo()
+ notifications.success(`Settings saved`)
+ } catch (e) {
+ notifications.error(e.message)
+ return
+ }
+
+ // Turn the save button grey when clicked
+ oidcSaveButtonDisabled = true
+ originalOidcDoc = cloneDeep(providers.oidc)
+ }
+
+ async function saveGoogle() {
+ if (!googleComplete) {
+ notifications.error(
+ `Please fill in all required ${ConfigTypes.Google} fields`
+ )
+ return
+ }
+
+ const google = providers.google
+
+ try {
+ const res = await saveConfig(google)
+ providers[res.type]._rev = res._rev
+ providers[res.type]._id = res._id
+ notifications.success(`Settings saved`)
+ } catch (e) {
+ notifications.error(e.message)
+ return
+ }
+
+ googleSaveButtonDisabled = true
+ originalGoogleDoc = cloneDeep(providers.google)
}
let defaultScopes = ["profile", "email", "offline_access"]
@@ -266,7 +281,7 @@
if (!googleDoc?._id) {
providers.google = {
type: ConfigTypes.Google,
- config: { activated: true },
+ config: { activated: false },
}
originalGoogleDoc = cloneDeep(googleDoc)
} else {
@@ -310,7 +325,7 @@
if (!oidcDoc?._id) {
providers.oidc = {
type: ConfigTypes.OIDC,
- config: { configs: [{ activated: true, scopes: defaultScopes }] },
+ config: { configs: [{ activated: false, scopes: defaultScopes }] },
}
} else {
originalOidcDoc = cloneDeep(oidcDoc)
@@ -413,7 +428,7 @@
@@ -575,11 +590,7 @@
-
diff --git a/packages/builder/src/stores/portal/organisation.js b/packages/builder/src/stores/portal/organisation.js
index c8b62bb2bc..36bf98b0ba 100644
--- a/packages/builder/src/stores/portal/organisation.js
+++ b/packages/builder/src/stores/portal/organisation.js
@@ -1,6 +1,7 @@
import { writable, get } from "svelte/store"
import { API } from "api"
import { auth } from "stores/portal"
+import _ from "lodash"
const DEFAULT_CONFIG = {
platformUrl: "",
@@ -26,14 +27,14 @@ export function createOrganisationStore() {
async function save(config) {
// Delete non-persisted fields
- const storeConfig = get(store)
+ const storeConfig = _.cloneDeep(get(store))
delete storeConfig.oidc
delete storeConfig.google
delete storeConfig.oidcCallbackUrl
delete storeConfig.googleCallbackUrl
await API.saveConfig({
type: "settings",
- config: { ...get(store), ...config },
+ config: { ...storeConfig, ...config },
})
await init()
}
diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts
index f4bba21e0f..6e86ed2674 100644
--- a/packages/types/src/documents/global/config.ts
+++ b/packages/types/src/documents/global/config.ts
@@ -36,6 +36,9 @@ export interface SettingsConfig extends Config {
config: SettingsInnerConfig
}
+export type SSOConfigType = ConfigType.GOOGLE | ConfigType.OIDC
+export type SSOConfig = GoogleInnerConfig | OIDCInnerConfig
+
export interface GoogleInnerConfig {
clientID: string
clientSecret: string
@@ -60,6 +63,10 @@ export interface OIDCStrategyConfiguration {
callbackURL: string
}
+export interface OIDCConfigs {
+ configs: OIDCInnerConfig[]
+}
+
export interface OIDCInnerConfig {
configUrl: string
clientID: string
@@ -72,9 +79,7 @@ export interface OIDCInnerConfig {
}
export interface OIDCConfig extends Config {
- config: {
- configs: OIDCInnerConfig[]
- }
+ config: OIDCConfigs
}
export interface OIDCWellKnownConfig {
diff --git a/packages/worker/src/api/controllers/global/configs.ts b/packages/worker/src/api/controllers/global/configs.ts
index 1ea0f52c96..02459855c2 100644
--- a/packages/worker/src/api/controllers/global/configs.ts
+++ b/packages/worker/src/api/controllers/global/configs.ts
@@ -17,10 +17,15 @@ import {
Ctx,
GetPublicOIDCConfigResponse,
GetPublicSettingsResponse,
+ GoogleInnerConfig,
isGoogleConfig,
isOIDCConfig,
isSettingsConfig,
isSMTPConfig,
+ OIDCConfigs,
+ SettingsInnerConfig,
+ SSOConfig,
+ SSOConfigType,
UserCtx,
} from "@budibase/types"
import * as pro from "@budibase/pro"
@@ -119,6 +124,61 @@ const getEventFns = async (config: Config, existing?: Config) => {
return fns
}
+type SSOConfigs = { [key in SSOConfigType]: SSOConfig | undefined }
+
+async function getSSOConfigs(): Promise {
+ 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()
+ }
+ return !!Object.values(ssoConfigs).find(c => c?.activated)
+}
+
+async function verifySettingsConfig(config: SettingsInnerConfig) {
+ if (config.isSSOEnforced) {
+ const valid = await hasActivatedConfig()
+ if (!valid) {
+ 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) {
const body = ctx.request.body
const type = body.type
@@ -133,10 +193,19 @@ export async function save(ctx: UserCtx) {
try {
// verify the configuration
- switch (config.type) {
+ 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)
diff --git a/packages/worker/src/api/routes/global/configs.ts b/packages/worker/src/api/routes/global/configs.ts
index 922bcea212..88c6bfbe7d 100644
--- a/packages/worker/src/api/routes/global/configs.ts
+++ b/packages/worker/src/api/routes/global/configs.ts
@@ -34,8 +34,8 @@ function settingValidation() {
function googleValidation() {
// prettier-ignore
return Joi.object({
- clientID: Joi.when('activated', { is: true, then: Joi.string().required() }),
- clientSecret: Joi.when('activated', { is: true, then: Joi.string().required() }),
+ clientID: Joi.string().required(),
+ clientSecret: Joi.string().required(),
activated: Joi.boolean().required(),
}).unknown(true)
}
@@ -45,12 +45,12 @@ function oidcValidation() {
return Joi.object({
configs: Joi.array().items(
Joi.object({
- clientID: Joi.when('activated', { is: true, then: Joi.string().required() }),
- clientSecret: Joi.when('activated', { is: true, then: Joi.string().required() }),
- configUrl: Joi.when('activated', { is: true, then: Joi.string().required() }),
+ clientID: Joi.string().required(),
+ clientSecret: Joi.string().required(),
+ configUrl: Joi.string().required(),
logo: Joi.string().allow("", null),
name: Joi.string().allow("", null),
- uuid: Joi.when('activated', { is: true, then: Joi.string().required() }),
+ uuid: Joi.string().required(),
activated: Joi.boolean().required(),
scopes: Joi.array().optional()
})
diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts
index 69861684eb..4fd8a82161 100644
--- a/packages/worker/src/utilities/email.ts
+++ b/packages/worker/src/utilities/email.ts
@@ -203,7 +203,7 @@ export async function sendEmail(
* @param {object} config an SMTP configuration - this is based on the nodemailer API.
* @return {Promise} returns true if the configuration is valid.
*/
-export async function verifyConfig(config: any) {
+export async function verifyConfig(config: SMTPInnerConfig) {
const transport = createSMTPTransport(config)
await transport.verify()
}