From cf13853f092f1d7ef87615408afcf3472bbe1e8b Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 12 Nov 2021 13:31:55 +0000 Subject: [PATCH] Fixes for google sso, cloud email url and cloud logo updates --- packages/auth/src/db/utils.js | 41 ++++++++++++- packages/auth/src/environment.js | 1 + packages/auth/src/objectStore/utils.js | 1 + packages/bbui/src/Label/Label.svelte | 57 ++++++++++++++++++- packages/bbui/src/Tooltip/Tooltip.svelte | 20 +++++-- .../builder/portal/manage/auth/index.svelte | 39 +++++++------ .../portal/settings/organisation.svelte | 7 ++- .../builder/src/stores/portal/organisation.js | 11 +++- packages/server/src/integrations/postgres.ts | 10 ++-- .../worker/src/api/controllers/global/auth.js | 34 +++++++---- .../src/api/controllers/global/configs.js | 25 +++++++- .../worker/src/api/routes/tests/auth.spec.js | 2 +- packages/worker/src/utilities/templates.js | 4 -- 13 files changed, 197 insertions(+), 55 deletions(-) diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index fa162603e6..4da5d2d410 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -1,6 +1,6 @@ const { newid } = require("../hashing") const Replication = require("./Replication") -const { DEFAULT_TENANT_ID } = require("../constants") +const { DEFAULT_TENANT_ID, Configs } = require("../constants") const env = require("../environment") const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants") const { getTenantId, getTenantIDFromAppID } = require("../tenancy") @@ -322,13 +322,50 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) { } // Find the config with the most granular scope based on context - const scopedConfig = response.rows.sort( + let scopedConfig = response.rows.sort( (a, b) => determineScore(a) - determineScore(b) )[0] + // custom logic for settings doc + // always provide the platform URL + if (type === Configs.SETTINGS) { + if (scopedConfig && scopedConfig.doc) { + scopedConfig.doc.config.platformUrl = await getPlatformUrl( + scopedConfig.doc.config + ) + } else { + scopedConfig = { + doc: { + config: { + platformUrl: await getPlatformUrl(), + }, + }, + } + } + } + return scopedConfig && scopedConfig.doc } +const getPlatformUrl = async settings => { + let platformUrl = env.PLATFORM_URL + + if (!env.SELF_HOSTED && env.MULTI_TENANCY) { + // cloud and multi tenant - add the tenant to the default platform url + const tenantId = getTenantId() + if (!platformUrl.includes("localhost:")) { + platformUrl = platformUrl.replace("://", `://${tenantId}.`) + } + } else { + // self hosted - check for platform url override + if (settings && settings.platformUrl) { + platformUrl = settings.platformUrl + } + } + + return platformUrl ? platformUrl : "http://localhost:10000" +} + async function getScopedConfig(db, params) { const configDoc = await getScopedFullConfig(db, params) return configDoc && configDoc.config ? configDoc.config : configDoc diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js index c36b469c4e..c26ad1c199 100644 --- a/packages/auth/src/environment.js +++ b/packages/auth/src/environment.js @@ -25,6 +25,7 @@ module.exports = { DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, + PLATFORM_URL: process.env.PLATFORM_URL, isTest, _set(key, value) { process.env[key] = value diff --git a/packages/auth/src/objectStore/utils.js b/packages/auth/src/objectStore/utils.js index 41fa18d99e..1634a24981 100644 --- a/packages/auth/src/objectStore/utils.js +++ b/packages/auth/src/objectStore/utils.js @@ -6,6 +6,7 @@ exports.ObjectStoreBuckets = { APPS: "prod-budi-app-assets", TEMPLATES: "templates", GLOBAL: "global", + GLOBAL_CLOUD: "prod-budi-tenant-uploads", } exports.budibaseTempDir = function () { diff --git a/packages/bbui/src/Label/Label.svelte b/packages/bbui/src/Label/Label.svelte index 3ed18dc6f5..a3a94b2836 100644 --- a/packages/bbui/src/Label/Label.svelte +++ b/packages/bbui/src/Label/Label.svelte @@ -1,16 +1,67 @@ - +{#if tooltip} +
+ +
+
(showTooltip = true)} + on:mouseleave={() => (showTooltip = false)} + > + +
+ {#if showTooltip} +
+ +
+ {/if} +
+
+{:else} + +{/if} diff --git a/packages/bbui/src/Tooltip/Tooltip.svelte b/packages/bbui/src/Tooltip/Tooltip.svelte index a4b1d4ff59..408a9ddf8f 100644 --- a/packages/bbui/src/Tooltip/Tooltip.svelte +++ b/packages/bbui/src/Tooltip/Tooltip.svelte @@ -3,12 +3,22 @@ export let direction = "top" export let text = "" + export let textWrapping = false - - -
+ +{#if textWrapping} + {text} -
-
+ +{:else} + + + +
+ {text} + +
+
+{/if} diff --git a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte index c2445e14ae..20d30fdfbb 100644 --- a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte @@ -21,26 +21,25 @@ } from "@budibase/bbui" import { onMount } from "svelte" import api from "builderStore/api" - import { organisation, auth, admin } from "stores/portal" + import { organisation, admin } from "stores/portal" import { uuid } from "builderStore/uuid" import analytics, { Events } from "analytics" - $: tenantId = $auth.tenantId - $: multiTenancyEnabled = $admin.multiTenancy - const ConfigTypes = { Google: "google", OIDC: "oidc", } - function callbackUrl(tenantId, end) { - let url = `/api/global/auth` - if (multiTenancyEnabled && tenantId) { - url += `/${tenantId}` - } - url += end - return url - } + // Some older google configs contain a manually specified value - retain the functionality to edit the field + // When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change + $: googleCallbackUrl = undefined + $: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl + + // Indicate to user that callback is based on platform url + // If there is an existing value, indicate that it may be removed to return to default behaviour + $: googleCallbackTooltip = googleCallbackReadonly + ? "Vist the organisation page to update the platform URL" + : "Leave blank to use the default callback URL" $: GoogleConfigFields = { Google: [ @@ -49,8 +48,9 @@ { name: "callbackURL", label: "Callback URL", - readonly: true, - placeholder: callbackUrl(tenantId, "/google/callback"), + readonly: googleCallbackReadonly, + tooltip: googleCallbackTooltip, + placeholder: $organisation.googleCallbackUrl, }, ], } @@ -62,9 +62,10 @@ { name: "clientSecret", label: "Client Secret" }, { name: "callbackURL", - label: "Callback URL", readonly: true, - placeholder: callbackUrl(tenantId, "/oidc/callback"), + tooltip: "Vist the organisation page to update the platform URL", + label: "Callback URL", + placeholder: $organisation.oidcCallbackUrl, }, ], } @@ -241,6 +242,8 @@ providers.google = googleDoc } + googleCallbackUrl = providers?.google?.config?.callbackURL + //Get the list of user uploaded logos and push it to the dropdown options. //This needs to be done before the config call so they're available when the dropdown renders const res = await api.get(`/api/global/configs/logos_oidc`) @@ -308,7 +311,7 @@ {#each GoogleConfigFields.Google as field}
- + {#each OIDCConfigFields.Oidc as field}
- +
- +
@@ -135,6 +139,7 @@ .field { display: grid; grid-template-columns: 100px 1fr; + grid-gap: var(--spacing-l); align-items: center; } .file { diff --git a/packages/builder/src/stores/portal/organisation.js b/packages/builder/src/stores/portal/organisation.js index 03bfa6ca28..21a110c54a 100644 --- a/packages/builder/src/stores/portal/organisation.js +++ b/packages/builder/src/stores/portal/organisation.js @@ -3,12 +3,14 @@ import api from "builderStore/api" import { auth } from "stores/portal" const DEFAULT_CONFIG = { - platformUrl: "http://localhost:10000", + platformUrl: "", logoUrl: undefined, docsUrl: undefined, company: "Budibase", oidc: undefined, google: undefined, + oidcCallbackUrl: "", + googleCallbackUrl: "", } export function createOrganisationStore() { @@ -28,6 +30,13 @@ export function createOrganisationStore() { } async function save(config) { + // delete non-persisted fields + const storeConfig = get(store) + delete storeConfig.oidc + delete storeConfig.google + delete storeConfig.oidcCallbackUrl + delete storeConfig.googleCallbackUrl + const res = await api.post("/api/global/configs", { type: "settings", config: { ...get(store), ...config }, diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts index e42681873d..b2e48ad540 100644 --- a/packages/server/src/integrations/postgres.ts +++ b/packages/server/src/integrations/postgres.ts @@ -130,7 +130,7 @@ module PostgresModule { public tables: Record = {} public schemaErrors: Record = {} - COLUMNS_SQL!: string + COLUMNS_SQL!: string PRIMARY_KEYS_SQL = ` select tc.table_schema, tc.table_name, kc.column_name as primary_key @@ -165,11 +165,11 @@ module PostgresModule { setSchema() { if (!this.config.schema) { - this.config.schema = 'public' + this.config.schema = "public" } - this.client.on('connect', (client: any) => { - client.query(`SET search_path TO ${this.config.schema}`); - }); + this.client.on("connect", (client: any) => { + client.query(`SET search_path TO ${this.config.schema}`) + }) this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'` } diff --git a/packages/worker/src/api/controllers/global/auth.js b/packages/worker/src/api/controllers/global/auth.js index 36db346dbd..5f7e656f7d 100644 --- a/packages/worker/src/api/controllers/global/auth.js +++ b/packages/worker/src/api/controllers/global/auth.js @@ -1,4 +1,5 @@ const authPkg = require("@budibase/auth") +const { getScopedConfig } = require("@budibase/auth/db") const { google } = require("@budibase/auth/src/middleware") const { oidc } = require("@budibase/auth/src/middleware") const { Configs, EmailTemplatePurpose } = require("../../../constants") @@ -21,17 +22,32 @@ const { } = require("@budibase/auth/tenancy") const env = require("../../../environment") -function googleCallbackUrl(config) { +const ssoCallbackUrl = async (config, type) => { // incase there is a callback URL from before if (config && config.callbackURL) { return config.callbackURL } + + const db = getGlobalDB() + const publicConfig = await getScopedConfig(db, { + type: Configs.SETTINGS, + }) + let callbackUrl = `/api/global/auth` if (isMultiTenant()) { callbackUrl += `/${getTenantId()}` } - callbackUrl += `/google/callback` - return callbackUrl + callbackUrl += `/${type}/callback` + + return `${publicConfig.platformUrl}${callbackUrl}` +} + +exports.googleCallbackUrl = async config => { + return ssoCallbackUrl(config, "google") +} + +exports.oidcCallbackUrl = async config => { + return ssoCallbackUrl(config, "oidc") } async function authInternal(ctx, user, err = null, info = null) { @@ -152,7 +168,7 @@ exports.googlePreAuth = async (ctx, next) => { type: Configs.GOOGLE, workspace: ctx.query.workspace, }) - let callbackUrl = googleCallbackUrl(config) + let callbackUrl = await exports.googleCallbackUrl(config) const strategy = await google.strategyFactory(config, callbackUrl) return passport.authenticate(strategy, { @@ -167,7 +183,7 @@ exports.googleAuth = async (ctx, next) => { type: Configs.GOOGLE, workspace: ctx.query.workspace, }) - const callbackUrl = googleCallbackUrl(config) + const callbackUrl = await exports.googleCallbackUrl(config) const strategy = await google.strategyFactory(config, callbackUrl) return passport.authenticate( @@ -189,13 +205,7 @@ async function oidcStrategyFactory(ctx, configId) { }) const chosenConfig = config.configs.filter(c => c.uuid === configId)[0] - - const protocol = env.NODE_ENV === "production" ? "https" : "http" - let callbackUrl = `${protocol}://${ctx.host}/api/global/auth` - if (isMultiTenant()) { - callbackUrl += `/${getTenantId()}` - } - callbackUrl += `/oidc/callback` + let callbackUrl = await exports.oidcCallbackUrl(chosenConfig) return oidc.strategyFactory(chosenConfig, callbackUrl) } diff --git a/packages/worker/src/api/controllers/global/configs.js b/packages/worker/src/api/controllers/global/configs.js index c0c300e4db..b6fcb9ee7d 100644 --- a/packages/worker/src/api/controllers/global/configs.js +++ b/packages/worker/src/api/controllers/global/configs.js @@ -9,8 +9,11 @@ const { Configs } = require("../../../constants") const email = require("../../../utilities/email") const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore const CouchDB = require("../../../db") -const { getGlobalDB } = require("@budibase/auth/tenancy") +const { getGlobalDB, getTenantId } = require("@budibase/auth/tenancy") const env = require("../../../environment") +const { googleCallbackUrl, oidcCallbackUrl } = require("./auth") + +const BB_TENANT_CDN = "https://tenants.cdn.budi.live" exports.save = async function (ctx) { const db = getGlobalDB() @@ -155,6 +158,10 @@ exports.publicSettings = async function (ctx) { config.config.google = false } + // 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 @@ -182,7 +189,13 @@ exports.upload = async function (ctx) { bucket = ObjectStoreBuckets.GLOBAL_CLOUD } - const key = `${type}/${name}` + let key + if (env.MULTI_TENANCY) { + key = `${getTenantId()}/${type}/${name}` + } else { + key = `${type}/${name}` + } + await upload({ bucket, filename: key, @@ -200,7 +213,13 @@ exports.upload = async function (ctx) { config: {}, } } - const url = `/${bucket}/${key}` + let url + if (env.SELF_HOSTED) { + url = `/${bucket}/${key}` + } else { + url = `${BB_TENANT_CDN}/${key}` + } + cfgStructure.config[`${name}`] = url // write back to db with url updated await db.put(cfgStructure) diff --git a/packages/worker/src/api/routes/tests/auth.spec.js b/packages/worker/src/api/routes/tests/auth.spec.js index bb66c547c4..13a93f9cfb 100644 --- a/packages/worker/src/api/routes/tests/auth.spec.js +++ b/packages/worker/src/api/routes/tests/auth.spec.js @@ -76,7 +76,7 @@ describe("/api/global/auth", () => { afterEach(() => { expect(strategyFactory).toBeCalledWith( chosenConfig, - `http://127.0.0.1:4003/api/global/auth/${TENANT_ID}/oidc/callback` // calculated url + `http://localhost:10000/api/global/auth/${TENANT_ID}/oidc/callback` ) }) diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.js index aa06ede3ba..f261d9ca33 100644 --- a/packages/worker/src/utilities/templates.js +++ b/packages/worker/src/utilities/templates.js @@ -6,7 +6,6 @@ const { EmailTemplatePurpose, } = require("../constants") const { checkSlashesInUrl } = require("./index") -const env = require("../environment") const { getGlobalDB, addTenantToUrl } = require("@budibase/auth/tenancy") const BASE_COMPANY = "Budibase" @@ -14,9 +13,6 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => { const db = getGlobalDB() // TODO: use more granular settings in the future if required let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {} - if (!settings || !settings.platformUrl) { - settings.platformUrl = env.PLATFORM_URL - } const URL = settings.platformUrl const context = { [InternalTemplateBindings.LOGO_URL]: