Merge pull request #3354 from Budibase/rory/fixes-platform-url

Fixes for google sso, cloud email url and cloud logo updates
This commit is contained in:
Rory Powell 2021-11-16 13:46:06 +00:00 committed by GitHub
commit 4235aba7a2
12 changed files with 192 additions and 50 deletions

View File

@ -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")
@ -363,13 +363,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

View File

@ -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

View File

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

View File

@ -1,16 +1,67 @@
<script>
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 tooltip = ""
export let showTooltip = false
</script>
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
<slot />
</label>
{#if tooltip}
<div class="container">
<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>
label {
padding: 0;
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>

View File

@ -3,12 +3,22 @@
export let direction = "top"
export let text = ""
export let textWrapping = false
</script>
<span class="u-tooltip-showOnHover tooltip">
<slot />
<div class={`spectrum-Tooltip spectrum-Tooltip--${direction}`}>
<!-- Showing / Hiding a text wrapped tooltip should be handled outside the component -->
{#if textWrapping}
<span class="spectrum-Tooltip spectrum-Tooltip--{direction} is-open">
<span class="spectrum-Tooltip-label">{text}</span>
<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"
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 @@
<Layout gap="XS" noPadding>
{#each GoogleConfigFields.Google as field}
<div class="form-row">
<Label size="L">{field.label}</Label>
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
@ -346,7 +349,7 @@
<Layout gap="XS" noPadding>
{#each OIDCConfigFields.Oidc as field}
<div class="form-row">
<Label size="L">{field.label}</Label>
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input
bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly}

View File

@ -116,7 +116,11 @@
</Layout>
<div class="fields">
<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} />
</div>
</div>
@ -135,6 +139,7 @@
.field {
display: grid;
grid-template-columns: 100px 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
.file {

View File

@ -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 },

View File

@ -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)
}

View File

@ -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)

View File

@ -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`
)
})

View File

@ -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]: