1
0
Fork 0
mirror of synced 2024-07-02 21:10:43 +12:00
budibase/packages/worker/src/api/controllers/global/configs.ts
Rory Powell 786039055e File authentication using presigned URLs (#8883)
* file auth wip

* Private plugin bucket wip

* Add etag to logo request, fix circular dependency

* Resolve cyclic dependency

* Ensure browser doesn't cache custom component

* Global file fixes

* File fixes / remove permaUrl

* Add ctx local storage

* Enable presigned url to work from any host

* Test fix

* Self hosting w/ S3 + other fixes

* Fix for serving dev app in multi tenant

* Fix backup restore and import attachment url processing, never store attachment urls

* Test fixes

* Unit tests WIP

* Replace file path with base64 encoded private key

* Multi tenancy tests for files

* Environment files fixes

* Post-merge build and test fixes

* Add debug conditional to all jest logging and revert/fix prod minio proxy_pass

* Re-add default '/' minio route :(

* Rename iconFile -> iconFileName

* Add cloudfront vars to k8s templates

* Remove public bucket policy

* Remove duplicated test
2022-12-15 11:35:22 +00:00

422 lines
11 KiB
TypeScript

import * as email from "../../../utilities/email"
import env from "../../../environment"
import { googleCallbackUrl, oidcCallbackUrl } from "./auth"
import {
events,
cache,
objectStore,
tenancy,
db as dbCore,
env as coreEnv,
} from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users"
import {
Database,
Config as ConfigDoc,
ConfigType,
SSOType,
GoogleConfig,
OIDCConfig,
SettingsConfig,
isGoogleConfig,
isOIDCConfig,
isSettingsConfig,
isSMTPConfig,
Ctx,
UserCtx,
} from "@budibase/types"
const getEventFns = async (db: Database, config: ConfigDoc) => {
const fns = []
let existing
if (config._id) {
existing = await db.get(config._id)
}
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
}
export async function save(ctx: UserCtx) {
const db = tenancy.getGlobalDB()
const { type, workspace, user, config } = ctx.request.body
let eventFns = await getEventFns(db, ctx.request.body)
// Config does not exist yet
if (!ctx.request.body._id) {
ctx.request.body._id = dbCore.generateConfigID({
type,
workspace,
user,
})
}
try {
// verify the configuration
switch (type) {
case ConfigType.SMTP:
await email.verifyConfig(config)
break
}
} catch (err: any) {
ctx.throw(400, err)
}
try {
const response = await db.put(ctx.request.body)
await cache.bustCache(cache.CacheKey.CHECKLIST)
await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED)
for (const fn of eventFns) {
await fn()
}
ctx.body = {
type,
_id: response.id,
_rev: response.rev,
}
} catch (err: any) {
ctx.throw(400, err)
}
}
export async function fetch(ctx: UserCtx) {
const db = tenancy.getGlobalDB()
const response = await db.allDocs(
dbCore.getConfigParams(
{ type: ctx.params.type },
{
include_docs: true,
}
)
)
ctx.body = response.rows.map(row => row.doc)
}
/**
* Gets the most granular config for a particular configuration type.
* The hierarchy is type -> workspace -> user.
*/
export async function find(ctx: UserCtx) {
const db = tenancy.getGlobalDB()
const { userId, workspaceId } = ctx.query
if (workspaceId && userId) {
const workspace = await db.get(workspaceId as string)
const userInWorkspace = workspace.users.some(
(workspaceUser: any) => workspaceUser === userId
)
if (!ctx.user!.admin && !userInWorkspace) {
ctx.throw(400, `User is not in specified workspace: ${workspace}.`)
}
}
try {
// Find the config with the most granular scope based on context
const scopedConfig = await dbCore.getScopedFullConfig(db, {
type: ctx.params.type,
user: userId,
workspace: workspaceId,
})
if (scopedConfig) {
ctx.body = scopedConfig
} else {
// don't throw an error, there simply is nothing to return
ctx.body = {}
}
} catch (err: any) {
ctx.throw(err?.status || 400, err)
}
}
export async function publicOidc(ctx: Ctx) {
const db = tenancy.getGlobalDB()
try {
// Find the config with the most granular scope based on context
const oidcConfig: OIDCConfig = await dbCore.getScopedFullConfig(db, {
type: ConfigType.OIDC,
})
if (!oidcConfig) {
ctx.body = {}
} else {
ctx.body = oidcConfig.config.configs.map(config => ({
logo: config.logo,
name: config.name,
uuid: config.uuid,
}))
}
} catch (err: any) {
ctx.throw(err.status, err)
}
}
export async function publicSettings(ctx: Ctx) {
const db = tenancy.getGlobalDB()
try {
// Find the config with the most granular scope based on context
const publicConfig = await dbCore.getScopedFullConfig(db, {
type: ConfigType.SETTINGS,
})
const googleConfig = await dbCore.getScopedFullConfig(db, {
type: ConfigType.GOOGLE,
})
const oidcConfig = await dbCore.getScopedFullConfig(db, {
type: ConfigType.OIDC,
})
let config
if (!publicConfig) {
config = {
config: {},
}
} else {
config = publicConfig
}
// enrich the logo url
// empty url means deleted
if (config.config.logoUrl !== "") {
config.config.logoUrl = objectStore.getGlobalFileUrl(
"settings",
"logoUrl",
config.config.logoUrlEtag
)
}
// google button flag
if (googleConfig && googleConfig.config) {
// activated by default for configs pre-activated flag
config.config.google =
googleConfig.config.activated == null || googleConfig.config.activated
} else {
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
} else {
config.config.oidc = false
}
ctx.body = config
} 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.")
}
const file = ctx.request.files.file
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
// TODO: right now this only does a global level
const db = tenancy.getGlobalDB()
let cfgStructure = await dbCore.getScopedFullConfig(db, { type })
if (!cfgStructure) {
cfgStructure = {
_id: dbCore.generateConfigID({ type }),
config: {},
}
}
// save the Etag for cache bursting
const etag = result.ETag
if (etag) {
cfgStructure.config[`${name}Etag`] = etag.replace(/"/g, "")
}
// save the file key
cfgStructure.config[`${name}`] = key
// write back to db
await db.put(cfgStructure)
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()
const { id, rev } = ctx.params
try {
await db.remove(id, rev)
await cache.destroy(cache.CacheKey.CHECKLIST)
ctx.body = { message: "Config deleted successfully" }
} catch (err: any) {
ctx.throw(err.status, err)
}
}
export async function configChecklist(ctx: Ctx) {
const db = tenancy.getGlobalDB()
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 })
}
// They have set up SMTP
const smtpConfig = await dbCore.getScopedFullConfig(db, {
type: ConfigType.SMTP,
})
// They have set up Google Auth
const googleConfig = await dbCore.getScopedFullConfig(db, {
type: ConfigType.GOOGLE,
})
// They have set up OIDC
const oidcConfig = await dbCore.getScopedFullConfig(db, {
type: ConfigType.OIDC,
})
// 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)
}
}