Fixes for google sso, cloud email url and cloud logo updates

This commit is contained in:
Rory Powell 2021-11-12 13:31:55 +00:00
parent 1f7207be7b
commit 6af8ab2dc0
13 changed files with 197 additions and 55 deletions

View File

@ -1,6 +1,6 @@
const { newid } = require("../hashing") const { newid } = require("../hashing")
const Replication = require("./Replication") const Replication = require("./Replication")
const { DEFAULT_TENANT_ID } = require("../constants") const { DEFAULT_TENANT_ID, Configs } = require("../constants")
const env = require("../environment") const env = require("../environment")
const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants") const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants")
const { getTenantId, getTenantIDFromAppID } = require("../tenancy") 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 // 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) (a, b) => determineScore(a) - determineScore(b)
)[0] )[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 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) { async function getScopedConfig(db, params) {
const configDoc = await getScopedFullConfig(db, params) const configDoc = await getScopedFullConfig(db, params)
return configDoc && configDoc.config ? configDoc.config : configDoc return configDoc && configDoc.config ? configDoc.config : configDoc

View File

@ -25,6 +25,7 @@ module.exports = {
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
PLATFORM_URL: process.env.PLATFORM_URL,
isTest, isTest,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value

View File

@ -6,6 +6,7 @@ exports.ObjectStoreBuckets = {
APPS: "prod-budi-app-assets", APPS: "prod-budi-app-assets",
TEMPLATES: "templates", TEMPLATES: "templates",
GLOBAL: "global", GLOBAL: "global",
GLOBAL_CLOUD: "prod-budi-tenant-uploads",
} }
exports.budibaseTempDir = function () { exports.budibaseTempDir = function () {

View File

@ -1,16 +1,67 @@
<script> <script>
import "@spectrum-css/fieldlabel/dist/index-vars.css" import "@spectrum-css/fieldlabel/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte"
import Icon from "../Icon/Icon.svelte"
export let size = "M" export let size = "M"
export let tooltip = ""
export let showTooltip = false
</script> </script>
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> {#if tooltip}
<slot /> <div class="container">
</label> <label
for=""
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}
>
<slot />
</label>
<div class="icon-container">
<div
class="icon"
on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
>
<Icon name="InfoOutline" size="S" disabled={true} />
</div>
{#if showTooltip}
<div class="tooltip">
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
</div>
{/if}
</div>
</div>
{:else}
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
<slot />
</label>
{/if}
<style> <style>
label { label {
padding: 0; padding: 0;
white-space: nowrap; white-space: nowrap;
} }
.container {
display: flex;
}
.icon-container {
position: relative;
display: flex;
justify-content: center;
margin-top: 1px;
margin-left: 5px;
margin-right: 5px;
}
.tooltip {
position: absolute;
display: flex;
justify-content: center;
top: 15px;
z-index: 1;
width: 160px;
}
.icon {
transform: scale(0.75);
}
</style> </style>

View File

@ -3,12 +3,22 @@
export let direction = "top" export let direction = "top"
export let text = "" export let text = ""
export let textWrapping = false
</script> </script>
<span class="u-tooltip-showOnHover tooltip"> <!-- Showing / Hiding a text wrapped tooltip should be handled outside the component -->
<slot /> {#if textWrapping}
<div class={`spectrum-Tooltip spectrum-Tooltip--${direction}`}> <span class="spectrum-Tooltip spectrum-Tooltip--{direction} is-open">
<span class="spectrum-Tooltip-label">{text}</span> <span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" /> <span class="spectrum-Tooltip-tip" />
</div> </span>
</span> {:else}
<!-- The default show on hover tooltip does not support text wrapping -->
<span class="u-tooltip-showOnHover tooltip">
<slot />
<div class={`spectrum-Tooltip spectrum-Tooltip--${direction}`}>
<span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" />
</div>
</span>
{/if}

View File

@ -21,26 +21,25 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import api from "builderStore/api" import api from "builderStore/api"
import { organisation, auth, admin } from "stores/portal" import { organisation, admin } from "stores/portal"
import { uuid } from "builderStore/uuid" import { uuid } from "builderStore/uuid"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
$: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy
const ConfigTypes = { const ConfigTypes = {
Google: "google", Google: "google",
OIDC: "oidc", OIDC: "oidc",
} }
function callbackUrl(tenantId, end) { // Some older google configs contain a manually specified value - retain the functionality to edit the field
let url = `/api/global/auth` // When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
if (multiTenancyEnabled && tenantId) { $: googleCallbackUrl = undefined
url += `/${tenantId}` $: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl
}
url += end // Indicate to user that callback is based on platform url
return 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 = { $: GoogleConfigFields = {
Google: [ Google: [
@ -49,8 +48,9 @@
{ {
name: "callbackURL", name: "callbackURL",
label: "Callback URL", label: "Callback URL",
readonly: true, readonly: googleCallbackReadonly,
placeholder: callbackUrl(tenantId, "/google/callback"), tooltip: googleCallbackTooltip,
placeholder: $organisation.googleCallbackUrl,
}, },
], ],
} }
@ -62,9 +62,10 @@
{ name: "clientSecret", label: "Client Secret" }, { name: "clientSecret", label: "Client Secret" },
{ {
name: "callbackURL", name: "callbackURL",
label: "Callback URL",
readonly: true, 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 providers.google = googleDoc
} }
googleCallbackUrl = providers?.google?.config?.callbackURL
//Get the list of user uploaded logos and push it to the dropdown options. //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 //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`) const res = await api.get(`/api/global/configs/logos_oidc`)
@ -308,7 +311,7 @@
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#each GoogleConfigFields.Google as field} {#each GoogleConfigFields.Google as field}
<div class="form-row"> <div class="form-row">
<Label size="L">{field.label}</Label> <Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input <Input
bind:value={providers.google.config[field.name]} bind:value={providers.google.config[field.name]}
readonly={field.readonly} readonly={field.readonly}
@ -346,7 +349,7 @@
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#each OIDCConfigFields.Oidc as field} {#each OIDCConfigFields.Oidc as field}
<div class="form-row"> <div class="form-row">
<Label size="L">{field.label}</Label> <Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input <Input
bind:value={providers.oidc.config.configs[0][field.name]} bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly} readonly={field.readonly}

View File

@ -116,7 +116,11 @@
</Layout> </Layout>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label size="L">Platform URL</Label> <Label
size="L"
tooltip={"Update the Platform URL to match your Budibase web URL. This keeps email templates and authentication configs up to date."}
>Platform URL</Label
>
<Input thin bind:value={$values.platformUrl} /> <Input thin bind:value={$values.platformUrl} />
</div> </div>
</div> </div>
@ -135,6 +139,7 @@
.field { .field {
display: grid; display: grid;
grid-template-columns: 100px 1fr; grid-template-columns: 100px 1fr;
grid-gap: var(--spacing-l);
align-items: center; align-items: center;
} }
.file { .file {

View File

@ -3,12 +3,14 @@ import api from "builderStore/api"
import { auth } from "stores/portal" import { auth } from "stores/portal"
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
platformUrl: "http://localhost:10000", platformUrl: "",
logoUrl: undefined, logoUrl: undefined,
docsUrl: undefined, docsUrl: undefined,
company: "Budibase", company: "Budibase",
oidc: undefined, oidc: undefined,
google: undefined, google: undefined,
oidcCallbackUrl: "",
googleCallbackUrl: "",
} }
export function createOrganisationStore() { export function createOrganisationStore() {
@ -28,6 +30,13 @@ export function createOrganisationStore() {
} }
async function save(config) { 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", { const res = await api.post("/api/global/configs", {
type: "settings", type: "settings",
config: { ...get(store), ...config }, config: { ...get(store), ...config },

View File

@ -130,7 +130,7 @@ module PostgresModule {
public tables: Record<string, Table> = {} public tables: Record<string, Table> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
COLUMNS_SQL!: string COLUMNS_SQL!: string
PRIMARY_KEYS_SQL = ` PRIMARY_KEYS_SQL = `
select tc.table_schema, tc.table_name, kc.column_name as primary_key select tc.table_schema, tc.table_name, kc.column_name as primary_key
@ -165,11 +165,11 @@ module PostgresModule {
setSchema() { setSchema() {
if (!this.config.schema) { if (!this.config.schema) {
this.config.schema = 'public' this.config.schema = "public"
} }
this.client.on('connect', (client: any) => { this.client.on("connect", (client: any) => {
client.query(`SET search_path TO ${this.config.schema}`); client.query(`SET search_path TO ${this.config.schema}`)
}); })
this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'` this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'`
} }

View File

@ -1,4 +1,5 @@
const authPkg = require("@budibase/auth") const authPkg = require("@budibase/auth")
const { getScopedConfig } = require("@budibase/auth/db")
const { google } = require("@budibase/auth/src/middleware") const { google } = require("@budibase/auth/src/middleware")
const { oidc } = require("@budibase/auth/src/middleware") const { oidc } = require("@budibase/auth/src/middleware")
const { Configs, EmailTemplatePurpose } = require("../../../constants") const { Configs, EmailTemplatePurpose } = require("../../../constants")
@ -21,17 +22,32 @@ const {
} = require("@budibase/auth/tenancy") } = require("@budibase/auth/tenancy")
const env = require("../../../environment") const env = require("../../../environment")
function googleCallbackUrl(config) { const ssoCallbackUrl = async (config, type) => {
// incase there is a callback URL from before // incase there is a callback URL from before
if (config && config.callbackURL) { if (config && config.callbackURL) {
return config.callbackURL return config.callbackURL
} }
const db = getGlobalDB()
const publicConfig = await getScopedConfig(db, {
type: Configs.SETTINGS,
})
let callbackUrl = `/api/global/auth` let callbackUrl = `/api/global/auth`
if (isMultiTenant()) { if (isMultiTenant()) {
callbackUrl += `/${getTenantId()}` callbackUrl += `/${getTenantId()}`
} }
callbackUrl += `/google/callback` callbackUrl += `/${type}/callback`
return callbackUrl
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) { async function authInternal(ctx, user, err = null, info = null) {
@ -152,7 +168,7 @@ exports.googlePreAuth = async (ctx, next) => {
type: Configs.GOOGLE, type: Configs.GOOGLE,
workspace: ctx.query.workspace, workspace: ctx.query.workspace,
}) })
let callbackUrl = googleCallbackUrl(config) let callbackUrl = await exports.googleCallbackUrl(config)
const strategy = await google.strategyFactory(config, callbackUrl) const strategy = await google.strategyFactory(config, callbackUrl)
return passport.authenticate(strategy, { return passport.authenticate(strategy, {
@ -167,7 +183,7 @@ exports.googleAuth = async (ctx, next) => {
type: Configs.GOOGLE, type: Configs.GOOGLE,
workspace: ctx.query.workspace, workspace: ctx.query.workspace,
}) })
const callbackUrl = googleCallbackUrl(config) const callbackUrl = await exports.googleCallbackUrl(config)
const strategy = await google.strategyFactory(config, callbackUrl) const strategy = await google.strategyFactory(config, callbackUrl)
return passport.authenticate( return passport.authenticate(
@ -189,13 +205,7 @@ async function oidcStrategyFactory(ctx, configId) {
}) })
const chosenConfig = config.configs.filter(c => c.uuid === configId)[0] const chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
let callbackUrl = await exports.oidcCallbackUrl(chosenConfig)
const protocol = env.NODE_ENV === "production" ? "https" : "http"
let callbackUrl = `${protocol}://${ctx.host}/api/global/auth`
if (isMultiTenant()) {
callbackUrl += `/${getTenantId()}`
}
callbackUrl += `/oidc/callback`
return oidc.strategyFactory(chosenConfig, callbackUrl) return oidc.strategyFactory(chosenConfig, callbackUrl)
} }

View File

@ -9,8 +9,11 @@ const { Configs } = require("../../../constants")
const email = require("../../../utilities/email") const email = require("../../../utilities/email")
const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const { getGlobalDB } = require("@budibase/auth/tenancy") const { getGlobalDB, getTenantId } = require("@budibase/auth/tenancy")
const env = require("../../../environment") const env = require("../../../environment")
const { googleCallbackUrl, oidcCallbackUrl } = require("./auth")
const BB_TENANT_CDN = "https://tenants.cdn.budi.live"
exports.save = async function (ctx) { exports.save = async function (ctx) {
const db = getGlobalDB() const db = getGlobalDB()
@ -155,6 +158,10 @@ exports.publicSettings = async function (ctx) {
config.config.google = false config.config.google = false
} }
// callback urls
config.config.oidcCallbackUrl = await oidcCallbackUrl()
config.config.googleCallbackUrl = await googleCallbackUrl()
// oidc button flag // oidc button flag
if (oidcConfig && oidcConfig.config) { if (oidcConfig && oidcConfig.config) {
config.config.oidc = oidcConfig.config.configs[0].activated config.config.oidc = oidcConfig.config.configs[0].activated
@ -182,7 +189,13 @@ exports.upload = async function (ctx) {
bucket = ObjectStoreBuckets.GLOBAL_CLOUD bucket = ObjectStoreBuckets.GLOBAL_CLOUD
} }
const key = `${type}/${name}` let key
if (env.MULTI_TENANCY) {
key = `${getTenantId()}/${type}/${name}`
} else {
key = `${type}/${name}`
}
await upload({ await upload({
bucket, bucket,
filename: key, filename: key,
@ -200,7 +213,13 @@ exports.upload = async function (ctx) {
config: {}, config: {},
} }
} }
const url = `/${bucket}/${key}` let url
if (env.SELF_HOSTED) {
url = `/${bucket}/${key}`
} else {
url = `${BB_TENANT_CDN}/${key}`
}
cfgStructure.config[`${name}`] = url cfgStructure.config[`${name}`] = url
// write back to db with url updated // write back to db with url updated
await db.put(cfgStructure) await db.put(cfgStructure)

View File

@ -76,7 +76,7 @@ describe("/api/global/auth", () => {
afterEach(() => { afterEach(() => {
expect(strategyFactory).toBeCalledWith( expect(strategyFactory).toBeCalledWith(
chosenConfig, 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`
) )
}) })

View File

@ -6,7 +6,6 @@ const {
EmailTemplatePurpose, EmailTemplatePurpose,
} = require("../constants") } = require("../constants")
const { checkSlashesInUrl } = require("./index") const { checkSlashesInUrl } = require("./index")
const env = require("../environment")
const { getGlobalDB, addTenantToUrl } = require("@budibase/auth/tenancy") const { getGlobalDB, addTenantToUrl } = require("@budibase/auth/tenancy")
const BASE_COMPANY = "Budibase" const BASE_COMPANY = "Budibase"
@ -14,9 +13,6 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => {
const db = getGlobalDB() const db = getGlobalDB()
// TODO: use more granular settings in the future if required // TODO: use more granular settings in the future if required
let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {} let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {}
if (!settings || !settings.platformUrl) {
settings.platformUrl = env.PLATFORM_URL
}
const URL = settings.platformUrl const URL = settings.platformUrl
const context = { const context = {
[InternalTemplateBindings.LOGO_URL]: [InternalTemplateBindings.LOGO_URL]: