Merge branch 'feature/audit-logs' of github.com:Budibase/budibase into feature/audit-logs
This commit is contained in:
commit
79da099c48
77 changed files with 1150 additions and 1041 deletions
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/nano": "10.1.1",
|
"@budibase/nano": "10.1.1",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||||
"@budibase/types": "2.3.18-alpha.12",
|
"@budibase/types": "2.3.18-alpha.13",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "2.2.0",
|
||||||
|
|
|
@ -2,25 +2,34 @@ const _passport = require("koa-passport")
|
||||||
const LocalStrategy = require("passport-local").Strategy
|
const LocalStrategy = require("passport-local").Strategy
|
||||||
const JwtStrategy = require("passport-jwt").Strategy
|
const JwtStrategy = require("passport-jwt").Strategy
|
||||||
import { getGlobalDB } from "../context"
|
import { getGlobalDB } from "../context"
|
||||||
const refresh = require("passport-oauth2-refresh")
|
import { Cookie } from "../constants"
|
||||||
import { Config, Cookie } from "../constants"
|
|
||||||
import { getScopedConfig } from "../db"
|
|
||||||
import { getSessionsForUser, invalidateSessions } from "../security/sessions"
|
import { getSessionsForUser, invalidateSessions } from "../security/sessions"
|
||||||
import {
|
import {
|
||||||
|
authenticated,
|
||||||
|
csrf,
|
||||||
|
google,
|
||||||
jwt as jwtPassport,
|
jwt as jwtPassport,
|
||||||
local,
|
local,
|
||||||
authenticated,
|
|
||||||
tenancy,
|
|
||||||
csrf,
|
|
||||||
oidc,
|
oidc,
|
||||||
google,
|
tenancy,
|
||||||
} from "../middleware"
|
} from "../middleware"
|
||||||
|
import * as userCache from "../cache/user"
|
||||||
import { invalidateUser } from "../cache/user"
|
import { invalidateUser } from "../cache/user"
|
||||||
import { PlatformLogoutOpts, User } from "@budibase/types"
|
import {
|
||||||
|
ConfigType,
|
||||||
|
GoogleInnerConfig,
|
||||||
|
OIDCInnerConfig,
|
||||||
|
PlatformLogoutOpts,
|
||||||
|
SSOProviderType,
|
||||||
|
User,
|
||||||
|
} from "@budibase/types"
|
||||||
import { logAlert } from "../logging"
|
import { logAlert } from "../logging"
|
||||||
import * as events from "../events"
|
import * as events from "../events"
|
||||||
import * as userCache from "../cache/user"
|
import * as configs from "../configs"
|
||||||
import { clearCookie, getCookie } from "../utils"
|
import { clearCookie, getCookie } from "../utils"
|
||||||
|
import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso"
|
||||||
|
|
||||||
|
const refresh = require("passport-oauth2-refresh")
|
||||||
export {
|
export {
|
||||||
auditLog,
|
auditLog,
|
||||||
authError,
|
authError,
|
||||||
|
@ -33,7 +42,6 @@ export {
|
||||||
google,
|
google,
|
||||||
oidc,
|
oidc,
|
||||||
} from "../middleware"
|
} from "../middleware"
|
||||||
import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso"
|
|
||||||
export const buildAuthMiddleware = authenticated
|
export const buildAuthMiddleware = authenticated
|
||||||
export const buildTenancyMiddleware = tenancy
|
export const buildTenancyMiddleware = tenancy
|
||||||
export const buildCsrfMiddleware = csrf
|
export const buildCsrfMiddleware = csrf
|
||||||
|
@ -63,11 +71,10 @@ _passport.deserializeUser(async (user: User, done: any) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function refreshOIDCAccessToken(
|
async function refreshOIDCAccessToken(
|
||||||
db: any,
|
chosenConfig: OIDCInnerConfig,
|
||||||
chosenConfig: any,
|
|
||||||
refreshToken: string
|
refreshToken: string
|
||||||
) {
|
): Promise<RefreshResponse> {
|
||||||
const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig)
|
const callbackUrl = await oidc.getCallbackUrl()
|
||||||
let enrichedConfig: any
|
let enrichedConfig: any
|
||||||
let strategy: any
|
let strategy: any
|
||||||
|
|
||||||
|
@ -90,7 +97,7 @@ async function refreshOIDCAccessToken(
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
refresh.requestNewAccessToken(
|
refresh.requestNewAccessToken(
|
||||||
Config.OIDC,
|
ConfigType.OIDC,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
(err: any, accessToken: string, refreshToken: any, params: any) => {
|
(err: any, accessToken: string, refreshToken: any, params: any) => {
|
||||||
resolve({ err, accessToken, refreshToken, params })
|
resolve({ err, accessToken, refreshToken, params })
|
||||||
|
@ -100,11 +107,10 @@ async function refreshOIDCAccessToken(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshGoogleAccessToken(
|
async function refreshGoogleAccessToken(
|
||||||
db: any,
|
config: GoogleInnerConfig,
|
||||||
config: any,
|
|
||||||
refreshToken: any
|
refreshToken: any
|
||||||
) {
|
): Promise<RefreshResponse> {
|
||||||
let callbackUrl = await google.getCallbackUrl(db, config)
|
let callbackUrl = await google.getCallbackUrl(config)
|
||||||
|
|
||||||
let strategy
|
let strategy
|
||||||
try {
|
try {
|
||||||
|
@ -124,7 +130,7 @@ async function refreshGoogleAccessToken(
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
refresh.requestNewAccessToken(
|
refresh.requestNewAccessToken(
|
||||||
Config.GOOGLE,
|
ConfigType.GOOGLE,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
(err: any, accessToken: string, refreshToken: string, params: any) => {
|
(err: any, accessToken: string, refreshToken: string, params: any) => {
|
||||||
resolve({ err, accessToken, refreshToken, params })
|
resolve({ err, accessToken, refreshToken, params })
|
||||||
|
@ -133,41 +139,37 @@ async function refreshGoogleAccessToken(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RefreshResponse {
|
||||||
|
err?: {
|
||||||
|
data?: string
|
||||||
|
}
|
||||||
|
accessToken?: string
|
||||||
|
refreshToken?: string
|
||||||
|
params?: any
|
||||||
|
}
|
||||||
|
|
||||||
export async function refreshOAuthToken(
|
export async function refreshOAuthToken(
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
configType: string,
|
providerType: SSOProviderType,
|
||||||
configId: string
|
configId?: string
|
||||||
) {
|
): Promise<RefreshResponse> {
|
||||||
const db = getGlobalDB()
|
switch (providerType) {
|
||||||
|
case SSOProviderType.OIDC:
|
||||||
const config = await getScopedConfig(db, {
|
if (!configId) {
|
||||||
type: configType,
|
return { err: { data: "OIDC config id not provided" } }
|
||||||
group: {},
|
}
|
||||||
})
|
const oidcConfig = await configs.getOIDCConfigById(configId)
|
||||||
|
if (!oidcConfig) {
|
||||||
let chosenConfig = {}
|
return { err: { data: "OIDC configuration not found" } }
|
||||||
let refreshResponse
|
}
|
||||||
if (configType === Config.OIDC) {
|
return refreshOIDCAccessToken(oidcConfig, refreshToken)
|
||||||
// configId - retrieved from cookie.
|
case SSOProviderType.GOOGLE:
|
||||||
chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
let googleConfig = await configs.getGoogleConfig()
|
||||||
if (!chosenConfig) {
|
if (!googleConfig) {
|
||||||
throw new Error("Invalid OIDC configuration")
|
return { err: { data: "Google configuration not found" } }
|
||||||
}
|
}
|
||||||
refreshResponse = await refreshOIDCAccessToken(
|
return refreshGoogleAccessToken(googleConfig, refreshToken)
|
||||||
db,
|
|
||||||
chosenConfig,
|
|
||||||
refreshToken
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
chosenConfig = config
|
|
||||||
refreshResponse = await refreshGoogleAccessToken(
|
|
||||||
db,
|
|
||||||
chosenConfig,
|
|
||||||
refreshToken
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshResponse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Refactor to use user save function instead to prevent the need for
|
// TODO: Refactor to use user save function instead to prevent the need for
|
||||||
|
@ -225,12 +227,6 @@ export async function platformLogout(opts: PlatformLogoutOpts) {
|
||||||
|
|
||||||
const sessionIds = sessions.map(({ sessionId }) => sessionId)
|
const sessionIds = sessions.map(({ sessionId }) => sessionId)
|
||||||
await invalidateSessions(userId, { sessionIds, reason: "logout" })
|
await invalidateSessions(userId, { sessionIds, reason: "logout" })
|
||||||
let user: User | undefined
|
await events.auth.logout(ctx.user?.email)
|
||||||
try {
|
|
||||||
user = await userCache.getUser(userId)
|
|
||||||
} catch {
|
|
||||||
user = undefined
|
|
||||||
}
|
|
||||||
await events.auth.logout(user?.email)
|
|
||||||
await userCache.invalidateUser(userId)
|
await userCache.invalidateUser(userId)
|
||||||
}
|
}
|
||||||
|
|
224
packages/backend-core/src/configs/configs.ts
Normal file
224
packages/backend-core/src/configs/configs.ts
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
import {
|
||||||
|
Config,
|
||||||
|
ConfigType,
|
||||||
|
GoogleConfig,
|
||||||
|
GoogleInnerConfig,
|
||||||
|
OIDCConfig,
|
||||||
|
OIDCInnerConfig,
|
||||||
|
SettingsConfig,
|
||||||
|
SettingsInnerConfig,
|
||||||
|
SMTPConfig,
|
||||||
|
SMTPInnerConfig,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { DocumentType, SEPARATOR } from "../constants"
|
||||||
|
import { CacheKey, TTL, withCache } from "../cache"
|
||||||
|
import * as context from "../context"
|
||||||
|
import env from "../environment"
|
||||||
|
import environment from "../environment"
|
||||||
|
|
||||||
|
// UTILS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new configuration ID.
|
||||||
|
* @returns {string} The new configuration ID which the config doc can be stored under.
|
||||||
|
*/
|
||||||
|
export function generateConfigID(type: ConfigType) {
|
||||||
|
return `${DocumentType.CONFIG}${SEPARATOR}${type}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConfig<T extends Config>(
|
||||||
|
type: ConfigType
|
||||||
|
): Promise<T | undefined> {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
try {
|
||||||
|
// await to catch error
|
||||||
|
const config = (await db.get(generateConfigID(type))) as T
|
||||||
|
return config
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.status === 404) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save(config: Config) {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
return db.put(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SETTINGS
|
||||||
|
|
||||||
|
export async function getSettingsConfigDoc(): Promise<SettingsConfig> {
|
||||||
|
let config = await getConfig<SettingsConfig>(ConfigType.SETTINGS)
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
config = {
|
||||||
|
_id: generateConfigID(ConfigType.GOOGLE),
|
||||||
|
type: ConfigType.SETTINGS,
|
||||||
|
config: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overridden fields
|
||||||
|
config.config.platformUrl = await getPlatformUrl({
|
||||||
|
tenantAware: true,
|
||||||
|
config: config.config,
|
||||||
|
})
|
||||||
|
config.config.analyticsEnabled = await analyticsEnabled({
|
||||||
|
config: config.config,
|
||||||
|
})
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSettingsConfig(): Promise<SettingsInnerConfig> {
|
||||||
|
return (await getSettingsConfigDoc()).config
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlatformUrl(
|
||||||
|
opts: { tenantAware: boolean; config?: SettingsInnerConfig } = {
|
||||||
|
tenantAware: true,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
let platformUrl = env.PLATFORM_URL || "http://localhost:10000"
|
||||||
|
|
||||||
|
if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) {
|
||||||
|
// cloud and multi tenant - add the tenant to the default platform url
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
if (!platformUrl.includes("localhost:")) {
|
||||||
|
platformUrl = platformUrl.replace("://", `://${tenantId}.`)
|
||||||
|
}
|
||||||
|
} else if (env.SELF_HOSTED) {
|
||||||
|
const config = opts?.config
|
||||||
|
? opts.config
|
||||||
|
: // direct to db to prevent infinite loop
|
||||||
|
(await getConfig<SettingsConfig>(ConfigType.SETTINGS))?.config
|
||||||
|
if (config?.platformUrl) {
|
||||||
|
platformUrl = config.platformUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return platformUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
export const analyticsEnabled = async (opts?: {
|
||||||
|
config?: SettingsInnerConfig
|
||||||
|
}) => {
|
||||||
|
// cloud - always use the environment variable
|
||||||
|
if (!env.SELF_HOSTED) {
|
||||||
|
return !!env.ENABLE_ANALYTICS
|
||||||
|
}
|
||||||
|
|
||||||
|
// self host - prefer the settings doc
|
||||||
|
// use cache as events have high throughput
|
||||||
|
const enabledInDB = await withCache(
|
||||||
|
CacheKey.ANALYTICS_ENABLED,
|
||||||
|
TTL.ONE_DAY,
|
||||||
|
async () => {
|
||||||
|
const config = opts?.config
|
||||||
|
? opts.config
|
||||||
|
: // direct to db to prevent infinite loop
|
||||||
|
(await getConfig<SettingsConfig>(ConfigType.SETTINGS))?.config
|
||||||
|
|
||||||
|
// need to do explicit checks in case the field is not set
|
||||||
|
if (config?.analyticsEnabled === false) {
|
||||||
|
return false
|
||||||
|
} else if (config?.analyticsEnabled === true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (enabledInDB !== undefined) {
|
||||||
|
return enabledInDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to the environment variable
|
||||||
|
// explicitly check for 0 or false here, undefined or otherwise is treated as true
|
||||||
|
const envEnabled: any = env.ENABLE_ANALYTICS
|
||||||
|
if (envEnabled === 0 || envEnabled === false) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOGLE
|
||||||
|
|
||||||
|
async function getGoogleConfigDoc(): Promise<GoogleConfig | undefined> {
|
||||||
|
return await getConfig<GoogleConfig>(ConfigType.GOOGLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGoogleConfig(): Promise<
|
||||||
|
GoogleInnerConfig | undefined
|
||||||
|
> {
|
||||||
|
const config = await getGoogleConfigDoc()
|
||||||
|
if (config) {
|
||||||
|
return config.config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use google fallback configuration from env variables
|
||||||
|
if (environment.GOOGLE_CLIENT_ID && environment.GOOGLE_CLIENT_SECRET) {
|
||||||
|
return {
|
||||||
|
clientID: environment.GOOGLE_CLIENT_ID!,
|
||||||
|
clientSecret: environment.GOOGLE_CLIENT_SECRET!,
|
||||||
|
activated: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDC
|
||||||
|
|
||||||
|
async function getOIDCConfigDoc(): Promise<OIDCConfig | undefined> {
|
||||||
|
return getConfig<OIDCConfig>(ConfigType.OIDC)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOIDCConfig(): Promise<OIDCInnerConfig | undefined> {
|
||||||
|
const config = (await getOIDCConfigDoc())?.config
|
||||||
|
// default to the 0th config
|
||||||
|
return config?.configs && config.configs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param configId The config id of the inner config to retrieve
|
||||||
|
*/
|
||||||
|
export async function getOIDCConfigById(
|
||||||
|
configId: string
|
||||||
|
): Promise<OIDCInnerConfig | undefined> {
|
||||||
|
const config = (await getConfig<OIDCConfig>(ConfigType.OIDC))?.config
|
||||||
|
return config && config.configs.filter((c: any) => c.uuid === configId)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
|
||||||
|
export async function getSMTPConfigDoc(): Promise<SMTPConfig | undefined> {
|
||||||
|
return getConfig<SMTPConfig>(ConfigType.SMTP)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSMTPConfig(
|
||||||
|
isAutomation?: boolean
|
||||||
|
): Promise<SMTPInnerConfig | undefined> {
|
||||||
|
const config = await getSMTPConfigDoc()
|
||||||
|
if (config) {
|
||||||
|
return config.config
|
||||||
|
}
|
||||||
|
|
||||||
|
// always allow fallback in self host
|
||||||
|
// in cloud don't allow for automations
|
||||||
|
const allowFallback = env.SELF_HOSTED || !isAutomation
|
||||||
|
|
||||||
|
// Use an SMTP fallback configuration from env variables
|
||||||
|
if (env.SMTP_FALLBACK_ENABLED && allowFallback) {
|
||||||
|
return {
|
||||||
|
port: env.SMTP_PORT,
|
||||||
|
host: env.SMTP_HOST!,
|
||||||
|
secure: false,
|
||||||
|
from: env.SMTP_FROM_ADDRESS!,
|
||||||
|
auth: {
|
||||||
|
user: env.SMTP_USER!,
|
||||||
|
pass: env.SMTP_PASSWORD!,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
packages/backend-core/src/configs/index.ts
Normal file
1
packages/backend-core/src/configs/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./configs"
|
116
packages/backend-core/src/configs/tests/configs.spec.ts
Normal file
116
packages/backend-core/src/configs/tests/configs.spec.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { DBTestConfiguration, generator, testEnv } from "../../../tests"
|
||||||
|
import { ConfigType } from "@budibase/types"
|
||||||
|
import env from "../../environment"
|
||||||
|
import * as configs from "../configs"
|
||||||
|
|
||||||
|
const DEFAULT_URL = "http://localhost:10000"
|
||||||
|
const ENV_URL = "http://env.com"
|
||||||
|
|
||||||
|
describe("configs", () => {
|
||||||
|
const config = new DBTestConfiguration()
|
||||||
|
|
||||||
|
const setDbPlatformUrl = async (dbUrl: string) => {
|
||||||
|
const settingsConfig = {
|
||||||
|
_id: configs.generateConfigID(ConfigType.SETTINGS),
|
||||||
|
type: ConfigType.SETTINGS,
|
||||||
|
config: {
|
||||||
|
platformUrl: dbUrl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await configs.save(settingsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
config.newTenant()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getPlatformUrl", () => {
|
||||||
|
describe("self host", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
testEnv.selfHosted()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets the default url", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const url = await configs.getPlatformUrl()
|
||||||
|
expect(url).toBe(DEFAULT_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets the platform url from the environment", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
env._set("PLATFORM_URL", ENV_URL)
|
||||||
|
const url = await configs.getPlatformUrl()
|
||||||
|
expect(url).toBe(ENV_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets the platform url from the database", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const dbUrl = generator.url()
|
||||||
|
await setDbPlatformUrl(dbUrl)
|
||||||
|
const url = await configs.getPlatformUrl()
|
||||||
|
expect(url).toBe(dbUrl)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("cloud", () => {
|
||||||
|
function getTenantAwareUrl() {
|
||||||
|
return `http://${config.tenantId}.env.com`
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
testEnv.cloudHosted()
|
||||||
|
testEnv.multiTenant()
|
||||||
|
|
||||||
|
env._set("PLATFORM_URL", ENV_URL)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets the platform url from the environment without tenancy", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const url = await configs.getPlatformUrl({ tenantAware: false })
|
||||||
|
expect(url).toBe(ENV_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets the platform url from the environment with tenancy", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const url = await configs.getPlatformUrl()
|
||||||
|
expect(url).toBe(getTenantAwareUrl())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("never gets the platform url from the database", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
await setDbPlatformUrl(generator.url())
|
||||||
|
const url = await configs.getPlatformUrl()
|
||||||
|
expect(url).toBe(getTenantAwareUrl())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getSettingsConfig", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
testEnv.selfHosted()
|
||||||
|
env._set("PLATFORM_URL", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns the platform url with an existing config", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const dbUrl = generator.url()
|
||||||
|
await setDbPlatformUrl(dbUrl)
|
||||||
|
const config = await configs.getSettingsConfig()
|
||||||
|
expect(config.platformUrl).toBe(dbUrl)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns the platform url without an existing config", async () => {
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const config = await configs.getSettingsConfig()
|
||||||
|
expect(config.platformUrl).toBe(DEFAULT_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,19 +1,13 @@
|
||||||
import { generator, DBTestConfiguration, testEnv } from "../../../tests"
|
|
||||||
import {
|
import {
|
||||||
getDevelopmentAppID,
|
getDevelopmentAppID,
|
||||||
getProdAppID,
|
getProdAppID,
|
||||||
isDevAppID,
|
isDevAppID,
|
||||||
isProdAppID,
|
isProdAppID,
|
||||||
} from "../conversions"
|
} from "../conversions"
|
||||||
import { generateAppID, getPlatformUrl, getScopedConfig } from "../utils"
|
import { generateAppID } from "../utils"
|
||||||
import * as context from "../../context"
|
|
||||||
import { Config } from "../../constants"
|
|
||||||
import env from "../../environment"
|
|
||||||
|
|
||||||
describe("utils", () => {
|
describe("utils", () => {
|
||||||
const config = new DBTestConfiguration()
|
describe("generateAppID", () => {
|
||||||
|
|
||||||
describe("app ID manipulation", () => {
|
|
||||||
function getID() {
|
function getID() {
|
||||||
const appId = generateAppID()
|
const appId = generateAppID()
|
||||||
const split = appId.split("_")
|
const split = appId.split("_")
|
||||||
|
@ -66,127 +60,4 @@ describe("utils", () => {
|
||||||
expect(isProdAppID(devAppId)).toEqual(false)
|
expect(isProdAppID(devAppId)).toEqual(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const DEFAULT_URL = "http://localhost:10000"
|
|
||||||
const ENV_URL = "http://env.com"
|
|
||||||
|
|
||||||
const setDbPlatformUrl = async (dbUrl: string) => {
|
|
||||||
const db = context.getGlobalDB()
|
|
||||||
await db.put({
|
|
||||||
_id: "config_settings",
|
|
||||||
type: Config.SETTINGS,
|
|
||||||
config: {
|
|
||||||
platformUrl: dbUrl,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearSettingsConfig = async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
const db = context.getGlobalDB()
|
|
||||||
try {
|
|
||||||
const config = await db.get("config_settings")
|
|
||||||
await db.remove("config_settings", config._rev)
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.status !== 404) {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("getPlatformUrl", () => {
|
|
||||||
describe("self host", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
testEnv.selfHosted()
|
|
||||||
await clearSettingsConfig()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("gets the default url", async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
const url = await getPlatformUrl()
|
|
||||||
expect(url).toBe(DEFAULT_URL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("gets the platform url from the environment", async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
env._set("PLATFORM_URL", ENV_URL)
|
|
||||||
const url = await getPlatformUrl()
|
|
||||||
expect(url).toBe(ENV_URL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("gets the platform url from the database", async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
const dbUrl = generator.url()
|
|
||||||
await setDbPlatformUrl(dbUrl)
|
|
||||||
const url = await getPlatformUrl()
|
|
||||||
expect(url).toBe(dbUrl)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("cloud", () => {
|
|
||||||
const TENANT_AWARE_URL = `http://${config.tenantId}.env.com`
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
testEnv.cloudHosted()
|
|
||||||
testEnv.multiTenant()
|
|
||||||
|
|
||||||
env._set("PLATFORM_URL", ENV_URL)
|
|
||||||
await clearSettingsConfig()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("gets the platform url from the environment without tenancy", async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
const url = await getPlatformUrl({ tenantAware: false })
|
|
||||||
expect(url).toBe(ENV_URL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("gets the platform url from the environment with tenancy", async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
const url = await getPlatformUrl()
|
|
||||||
expect(url).toBe(TENANT_AWARE_URL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("never gets the platform url from the database", async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
await setDbPlatformUrl(generator.url())
|
|
||||||
const url = await getPlatformUrl()
|
|
||||||
expect(url).toBe(TENANT_AWARE_URL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getScopedConfig", () => {
|
|
||||||
describe("settings config", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
env._set("SELF_HOSTED", 1)
|
|
||||||
env._set("PLATFORM_URL", "")
|
|
||||||
await clearSettingsConfig()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns the platform url with an existing config", async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
const dbUrl = generator.url()
|
|
||||||
await setDbPlatformUrl(dbUrl)
|
|
||||||
const db = context.getGlobalDB()
|
|
||||||
const config = await getScopedConfig(db, { type: Config.SETTINGS })
|
|
||||||
expect(config.platformUrl).toBe(dbUrl)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns the platform url without an existing config", async () => {
|
|
||||||
await config.doInTenant(async () => {
|
|
||||||
const db = context.getGlobalDB()
|
|
||||||
const config = await getScopedConfig(db, { type: Config.SETTINGS })
|
|
||||||
expect(config.platformUrl).toBe(DEFAULT_URL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,12 +9,11 @@ import {
|
||||||
InternalTable,
|
InternalTable,
|
||||||
APP_PREFIX,
|
APP_PREFIX,
|
||||||
} from "../constants"
|
} from "../constants"
|
||||||
import { getTenantId, getGlobalDB, getGlobalDBName } from "../context"
|
import { getTenantId, getGlobalDBName } from "../context"
|
||||||
import { doWithDB, directCouchAllDbs } from "./db"
|
import { doWithDB, directCouchAllDbs } from "./db"
|
||||||
import { getAppMetadata } from "../cache/appMetadata"
|
import { getAppMetadata } from "../cache/appMetadata"
|
||||||
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
|
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
|
||||||
import * as events from "../events"
|
import { App, Database } from "@budibase/types"
|
||||||
import { App, Database, ConfigType, isSettingsConfig } from "@budibase/types"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new app ID.
|
* Generates a new app ID.
|
||||||
|
@ -412,32 +411,6 @@ export async function dbExists(dbName: any) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a new configuration ID.
|
|
||||||
* @returns {string} The new configuration ID which the config doc can be stored under.
|
|
||||||
*/
|
|
||||||
export const generateConfigID = ({ type, workspace, user }: any) => {
|
|
||||||
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
|
|
||||||
|
|
||||||
return `${DocumentType.CONFIG}${SEPARATOR}${scope}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets parameters for retrieving configurations.
|
|
||||||
*/
|
|
||||||
export const getConfigParams = (
|
|
||||||
{ type, workspace, user }: any,
|
|
||||||
otherProps = {}
|
|
||||||
) => {
|
|
||||||
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...otherProps,
|
|
||||||
startkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}`,
|
|
||||||
endkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new dev info document ID - this is scoped to a user.
|
* Generates a new dev info document ID - this is scoped to a user.
|
||||||
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
|
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
|
||||||
|
@ -461,109 +434,6 @@ export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
|
||||||
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
|
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
|
|
||||||
* @param {Object} db - db instance to query
|
|
||||||
* @param {Object} scopes - the type, workspace and userID scopes of the configuration.
|
|
||||||
* @returns The most granular configuration document based on the scope.
|
|
||||||
*/
|
|
||||||
export const getScopedFullConfig = async function (
|
|
||||||
db: any,
|
|
||||||
{ type, user, workspace }: any
|
|
||||||
) {
|
|
||||||
const response = await db.allDocs(
|
|
||||||
getConfigParams(
|
|
||||||
{ type, user, workspace },
|
|
||||||
{
|
|
||||||
include_docs: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
function determineScore(row: any) {
|
|
||||||
const config = row.doc
|
|
||||||
|
|
||||||
// Config is specific to a user and a workspace
|
|
||||||
if (config._id.includes(generateConfigID({ type, user, workspace }))) {
|
|
||||||
return 4
|
|
||||||
} else if (config._id.includes(generateConfigID({ type, user }))) {
|
|
||||||
// Config is specific to a user only
|
|
||||||
return 3
|
|
||||||
} else if (config._id.includes(generateConfigID({ type, workspace }))) {
|
|
||||||
// Config is specific to a workspace only
|
|
||||||
return 2
|
|
||||||
} else if (config._id.includes(generateConfigID({ type }))) {
|
|
||||||
// Config is specific to a type only
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the config with the most granular scope based on context
|
|
||||||
let scopedConfig = response.rows.sort(
|
|
||||||
(a: any, b: any) => determineScore(a) - determineScore(b)
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
// custom logic for settings doc
|
|
||||||
if (type === ConfigType.SETTINGS) {
|
|
||||||
if (!scopedConfig || !scopedConfig.doc) {
|
|
||||||
// defaults
|
|
||||||
scopedConfig = {
|
|
||||||
doc: {
|
|
||||||
_id: generateConfigID({ type, user, workspace }),
|
|
||||||
type: ConfigType.SETTINGS,
|
|
||||||
config: {
|
|
||||||
platformUrl: await getPlatformUrl({ tenantAware: true }),
|
|
||||||
analyticsEnabled: await events.analytics.enabled(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// will always be true - use assertion function to get type access
|
|
||||||
if (isSettingsConfig(scopedConfig.doc)) {
|
|
||||||
// overrides affected by environment
|
|
||||||
scopedConfig.doc.config.platformUrl = await getPlatformUrl({
|
|
||||||
tenantAware: true,
|
|
||||||
})
|
|
||||||
scopedConfig.doc.config.analyticsEnabled =
|
|
||||||
await events.analytics.enabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return scopedConfig && scopedConfig.doc
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getPlatformUrl = async (opts = { tenantAware: true }) => {
|
|
||||||
let platformUrl = env.PLATFORM_URL || "http://localhost:10000"
|
|
||||||
|
|
||||||
if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) {
|
|
||||||
// cloud and multi tenant - add the tenant to the default platform url
|
|
||||||
const tenantId = getTenantId()
|
|
||||||
if (!platformUrl.includes("localhost:")) {
|
|
||||||
platformUrl = platformUrl.replace("://", `://${tenantId}.`)
|
|
||||||
}
|
|
||||||
} else if (env.SELF_HOSTED) {
|
|
||||||
const db = getGlobalDB()
|
|
||||||
// get the doc directly instead of with getScopedConfig to prevent loop
|
|
||||||
let settings
|
|
||||||
try {
|
|
||||||
settings = await db.get(generateConfigID({ type: ConfigType.SETTINGS }))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.status !== 404) {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// self hosted - check for platform url override
|
|
||||||
if (settings && settings.config && settings.config.platformUrl) {
|
|
||||||
platformUrl = settings.config.platformUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return platformUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pagination(
|
export function pagination(
|
||||||
data: any[],
|
data: any[],
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
|
@ -597,8 +467,3 @@ export function pagination(
|
||||||
nextPage,
|
nextPage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getScopedConfig(db: any, params: any) {
|
|
||||||
const configDoc = await getScopedFullConfig(db, params)
|
|
||||||
return configDoc && configDoc.config ? configDoc.config : configDoc
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,6 +28,8 @@ const DefaultBucketName = {
|
||||||
PLUGINS: "plugins",
|
PLUGINS: "plugins",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
||||||
|
|
||||||
const environment = {
|
const environment = {
|
||||||
isTest,
|
isTest,
|
||||||
isJest,
|
isJest,
|
||||||
|
@ -58,7 +60,7 @@ const environment = {
|
||||||
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
||||||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "",
|
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "",
|
||||||
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: selfHosted,
|
||||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||||
PLATFORM_URL: process.env.PLATFORM_URL || "",
|
PLATFORM_URL: process.env.PLATFORM_URL || "",
|
||||||
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
|
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
|
||||||
|
@ -84,6 +86,22 @@ const environment = {
|
||||||
DEPLOYMENT_ENVIRONMENT:
|
DEPLOYMENT_ENVIRONMENT:
|
||||||
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
||||||
ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true,
|
ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true,
|
||||||
|
ENABLE_AUDIT_LOG_IP_ADDR: process.env.ENABLE_AUDIT_LOG_IP_ADDR,
|
||||||
|
// smtp
|
||||||
|
SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED,
|
||||||
|
SMTP_USER: process.env.SMTP_USER,
|
||||||
|
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
|
||||||
|
SMTP_HOST: process.env.SMTP_HOST,
|
||||||
|
SMTP_PORT: parseInt(process.env.SMTP_PORT || ""),
|
||||||
|
SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS,
|
||||||
|
/**
|
||||||
|
* Enable to allow an admin user to login using a password.
|
||||||
|
* This can be useful to prevent lockout when configuring SSO.
|
||||||
|
* However, this should be turned OFF by default for security purposes.
|
||||||
|
*/
|
||||||
|
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
||||||
|
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||||
|
: false,
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -1,55 +1,6 @@
|
||||||
import env from "../environment"
|
import * as configs from "../configs"
|
||||||
import * as context from "../context"
|
|
||||||
import * as dbUtils from "../db/utils"
|
|
||||||
import { Config } from "../constants"
|
|
||||||
import { withCache, TTL, CacheKey } from "../cache"
|
|
||||||
|
|
||||||
|
// wrapper utility function
|
||||||
export const enabled = async () => {
|
export const enabled = async () => {
|
||||||
// cloud - always use the environment variable
|
return configs.analyticsEnabled()
|
||||||
if (!env.SELF_HOSTED) {
|
|
||||||
return !!env.ENABLE_ANALYTICS
|
|
||||||
}
|
|
||||||
|
|
||||||
// self host - prefer the settings doc
|
|
||||||
// use cache as events have high throughput
|
|
||||||
const enabledInDB = await withCache(
|
|
||||||
CacheKey.ANALYTICS_ENABLED,
|
|
||||||
TTL.ONE_DAY,
|
|
||||||
async () => {
|
|
||||||
const settings = await getSettingsDoc()
|
|
||||||
|
|
||||||
// need to do explicit checks in case the field is not set
|
|
||||||
if (settings?.config?.analyticsEnabled === false) {
|
|
||||||
return false
|
|
||||||
} else if (settings?.config?.analyticsEnabled === true) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (enabledInDB !== undefined) {
|
|
||||||
return enabledInDB
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback to the environment variable
|
|
||||||
// explicitly check for 0 or false here, undefined or otherwise is treated as true
|
|
||||||
const envEnabled: any = env.ENABLE_ANALYTICS
|
|
||||||
if (envEnabled === 0 || envEnabled === false) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSettingsDoc = async () => {
|
|
||||||
const db = context.getGlobalDB()
|
|
||||||
let settings
|
|
||||||
try {
|
|
||||||
settings = await db.get(dbUtils.generateConfigID({ type: Config.SETTINGS }))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.status !== 404) {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return settings
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { processors } from "./processors"
|
||||||
import identification from "./identification"
|
import identification from "./identification"
|
||||||
import * as backfill from "./backfill"
|
import * as backfill from "./backfill"
|
||||||
|
|
||||||
export function isAudited(event: Event) {
|
|
||||||
return !!AuditedEventFriendlyName[event]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const publishEvent = async (
|
export const publishEvent = async (
|
||||||
event: Event,
|
event: Event,
|
||||||
properties: any,
|
properties: any,
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
isCloudAccount,
|
isCloudAccount,
|
||||||
isSSOAccount,
|
isSSOAccount,
|
||||||
TenantGroup,
|
TenantGroup,
|
||||||
SettingsConfig,
|
|
||||||
CloudAccount,
|
CloudAccount,
|
||||||
UserIdentity,
|
UserIdentity,
|
||||||
InstallationGroup,
|
InstallationGroup,
|
||||||
|
@ -19,10 +18,9 @@ import {
|
||||||
isSSOUser,
|
isSSOUser,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { processors } from "./processors"
|
import { processors } from "./processors"
|
||||||
import * as dbUtils from "../db/utils"
|
|
||||||
import { Config } from "../constants"
|
|
||||||
import { newid } from "../utils"
|
import { newid } from "../utils"
|
||||||
import * as installation from "../installation"
|
import * as installation from "../installation"
|
||||||
|
import * as configs from "../configs"
|
||||||
import { withCache, TTL, CacheKey } from "../cache/generic"
|
import { withCache, TTL, CacheKey } from "../cache/generic"
|
||||||
|
|
||||||
const pkg = require("../../package.json")
|
const pkg = require("../../package.json")
|
||||||
|
@ -271,9 +269,7 @@ const getUniqueTenantId = async (tenantId: string): Promise<string> => {
|
||||||
return context.doInTenant(tenantId, () => {
|
return context.doInTenant(tenantId, () => {
|
||||||
return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => {
|
return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => {
|
||||||
const db = context.getGlobalDB()
|
const db = context.getGlobalDB()
|
||||||
const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, {
|
const config = await configs.getSettingsConfigDoc()
|
||||||
type: Config.SETTINGS,
|
|
||||||
})
|
|
||||||
|
|
||||||
let uniqueTenantId: string
|
let uniqueTenantId: string
|
||||||
if (config.config.uniqueTenantId) {
|
if (config.config.uniqueTenantId) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ export * as processors from "./processors"
|
||||||
export * as analytics from "./analytics"
|
export * as analytics from "./analytics"
|
||||||
export { default as identification } from "./identification"
|
export { default as identification } from "./identification"
|
||||||
export * as backfillCache from "./backfill"
|
export * as backfillCache from "./backfill"
|
||||||
export { isAudited } from "./events"
|
|
||||||
|
|
||||||
import { processors } from "./processors"
|
import { processors } from "./processors"
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,14 @@ import {
|
||||||
IdentityType,
|
IdentityType,
|
||||||
AuditLogQueueEvent,
|
AuditLogQueueEvent,
|
||||||
AuditLogFn,
|
AuditLogFn,
|
||||||
|
HostInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { EventProcessor } from "./types"
|
import { EventProcessor } from "./types"
|
||||||
import { getAppId } from "../../context"
|
import { getAppId } from "../../context"
|
||||||
import { isAudited } from "../events"
|
|
||||||
import BullQueue from "bull"
|
import BullQueue from "bull"
|
||||||
import { createQueue, JobQueue } from "../../queue"
|
import { createQueue, JobQueue } from "../../queue"
|
||||||
|
import { isAudited } from "../../utils"
|
||||||
|
import env from "../../environment"
|
||||||
|
|
||||||
export default class AuditLogsProcessor implements EventProcessor {
|
export default class AuditLogsProcessor implements EventProcessor {
|
||||||
static auditLogsEnabled = false
|
static auditLogsEnabled = false
|
||||||
|
@ -32,11 +34,20 @@ export default class AuditLogsProcessor implements EventProcessor {
|
||||||
}
|
}
|
||||||
delete properties.audited
|
delete properties.audited
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this feature is disabled by default due to privacy requirements
|
||||||
|
// in some countries - available as env var in-case it is desired
|
||||||
|
// in self host deployments
|
||||||
|
let hostInfo: HostInfo | undefined = {}
|
||||||
|
if (env.ENABLE_AUDIT_LOG_IP_ADDR) {
|
||||||
|
hostInfo = job.data.opts.hostInfo
|
||||||
|
}
|
||||||
|
|
||||||
await writeAuditLogs(job.data.event, properties, {
|
await writeAuditLogs(job.data.event, properties, {
|
||||||
userId: job.data.opts.userId,
|
userId: job.data.opts.userId,
|
||||||
timestamp: job.data.opts.timestamp,
|
timestamp: job.data.opts.timestamp,
|
||||||
appId: job.data.opts.appId,
|
appId: job.data.opts.appId,
|
||||||
hostInfo: job.data.opts.hostInfo,
|
hostInfo,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * as configs from "./configs"
|
||||||
export * as events from "./events"
|
export * as events from "./events"
|
||||||
export * as migrations from "./migrations"
|
export * as migrations from "./migrations"
|
||||||
export * as users from "./users"
|
export * as users from "./users"
|
||||||
|
|
|
@ -115,7 +115,8 @@ export default function (
|
||||||
authenticated = true
|
authenticated = true
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
authenticated = false
|
authenticated = false
|
||||||
console.error("Auth Error", err?.message || err)
|
console.error(`Auth Error: ${err.message}`)
|
||||||
|
console.error(err)
|
||||||
// remove the cookie as the user does not exist anymore
|
// remove the cookie as the user does not exist anymore
|
||||||
clearCookie(ctx, Cookie.Auth)
|
clearCookie(ctx, Cookie.Auth)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export async function errorHandling(ctx: any, next: any) {
|
||||||
|
|
||||||
if (status > 499 || env.ENABLE_4XX_HTTP_LOGGING) {
|
if (status > 499 || env.ENABLE_4XX_HTTP_LOGGING) {
|
||||||
ctx.log.error(err)
|
ctx.log.error(err)
|
||||||
|
console.trace(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = errors.getPublicError(err)
|
const error = errors.getPublicError(err)
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import * as google from "../sso/google"
|
import * as google from "../sso/google"
|
||||||
import { Cookie, Config } from "../../../constants"
|
import { Cookie } from "../../../constants"
|
||||||
import { clearCookie, getCookie } from "../../../utils"
|
import { clearCookie, getCookie } from "../../../utils"
|
||||||
import { getScopedConfig, getPlatformUrl, doWithDB } from "../../../db"
|
import { doWithDB } from "../../../db"
|
||||||
import environment from "../../../environment"
|
import * as configs from "../../../configs"
|
||||||
import { getGlobalDB } from "../../../context"
|
|
||||||
import { BBContext, Database, SSOProfile } from "@budibase/types"
|
import { BBContext, Database, SSOProfile } from "@budibase/types"
|
||||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
@ -13,18 +12,11 @@ type Passport = {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGoogleCreds() {
|
async function fetchGoogleCreds() {
|
||||||
// try and get the config from the tenant
|
const config = await configs.getGoogleConfig()
|
||||||
const db = getGlobalDB()
|
if (!config) {
|
||||||
const googleConfig = await getScopedConfig(db, {
|
throw new Error("No google configuration found")
|
||||||
type: Config.GOOGLE,
|
}
|
||||||
})
|
return config
|
||||||
// or fall back to env variables
|
|
||||||
return (
|
|
||||||
googleConfig || {
|
|
||||||
clientID: environment.GOOGLE_CLIENT_ID,
|
|
||||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function preAuth(
|
export async function preAuth(
|
||||||
|
@ -34,7 +26,7 @@ export async function preAuth(
|
||||||
) {
|
) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
const googleConfig = await fetchGoogleCreds()
|
const googleConfig = await fetchGoogleCreds()
|
||||||
const platformUrl = await getPlatformUrl({ tenantAware: false })
|
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
||||||
|
|
||||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
|
@ -61,7 +53,7 @@ export async function postAuth(
|
||||||
) {
|
) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
const config = await fetchGoogleCreds()
|
const config = await fetchGoogleCreds()
|
||||||
const platformUrl = await getPlatformUrl({ tenantAware: false })
|
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
||||||
|
|
||||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
|
const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
|
||||||
|
|
|
@ -2,12 +2,11 @@ import { ssoCallbackUrl } from "../utils"
|
||||||
import * as sso from "./sso"
|
import * as sso from "./sso"
|
||||||
import {
|
import {
|
||||||
ConfigType,
|
ConfigType,
|
||||||
GoogleConfig,
|
|
||||||
Database,
|
|
||||||
SSOProfile,
|
SSOProfile,
|
||||||
SSOAuthDetails,
|
SSOAuthDetails,
|
||||||
SSOProviderType,
|
SSOProviderType,
|
||||||
SaveSSOUserFunction,
|
SaveSSOUserFunction,
|
||||||
|
GoogleInnerConfig,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
|
||||||
|
@ -45,7 +44,7 @@ export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {
|
||||||
* @returns Dynamically configured Passport Google Strategy
|
* @returns Dynamically configured Passport Google Strategy
|
||||||
*/
|
*/
|
||||||
export async function strategyFactory(
|
export async function strategyFactory(
|
||||||
config: GoogleConfig["config"],
|
config: GoogleInnerConfig,
|
||||||
callbackUrl: string,
|
callbackUrl: string,
|
||||||
saveUserFn: SaveSSOUserFunction
|
saveUserFn: SaveSSOUserFunction
|
||||||
) {
|
) {
|
||||||
|
@ -73,9 +72,6 @@ export async function strategyFactory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCallbackUrl(
|
export async function getCallbackUrl(config: GoogleInnerConfig) {
|
||||||
db: Database,
|
return ssoCallbackUrl(ConfigType.GOOGLE, config)
|
||||||
config: { callbackURL?: string }
|
|
||||||
) {
|
|
||||||
return ssoCallbackUrl(db, config, ConfigType.GOOGLE)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { ssoCallbackUrl } from "../utils"
|
||||||
import {
|
import {
|
||||||
ConfigType,
|
ConfigType,
|
||||||
OIDCInnerConfig,
|
OIDCInnerConfig,
|
||||||
Database,
|
|
||||||
SSOProfile,
|
SSOProfile,
|
||||||
OIDCStrategyConfiguration,
|
OIDCStrategyConfiguration,
|
||||||
SSOAuthDetails,
|
SSOAuthDetails,
|
||||||
|
@ -157,9 +156,6 @@ export async function fetchStrategyConfig(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCallbackUrl(
|
export async function getCallbackUrl() {
|
||||||
db: Database,
|
return ssoCallbackUrl(ConfigType.OIDC)
|
||||||
config: { callbackURL?: string }
|
|
||||||
) {
|
|
||||||
return ssoCallbackUrl(db, config, ConfigType.OIDC)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { isMultiTenant, getTenantId } from "../../context"
|
import { getTenantId, isMultiTenant } from "../../context"
|
||||||
import { getScopedConfig } from "../../db"
|
import * as configs from "../../configs"
|
||||||
import { ConfigType, Database } from "@budibase/types"
|
import { ConfigType, GoogleInnerConfig } from "@budibase/types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to handle authentication errors.
|
* Utility to handle authentication errors.
|
||||||
|
@ -19,17 +19,14 @@ export function authError(done: Function, message: string, err?: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ssoCallbackUrl(
|
export async function ssoCallbackUrl(
|
||||||
db: Database,
|
type: ConfigType,
|
||||||
config?: { callbackURL?: string },
|
config?: GoogleInnerConfig
|
||||||
type?: ConfigType
|
|
||||||
) {
|
) {
|
||||||
// incase there is a callback URL from before
|
// incase there is a callback URL from before
|
||||||
if (config && config.callbackURL) {
|
if (config && (config as GoogleInnerConfig).callbackURL) {
|
||||||
return config.callbackURL
|
return (config as GoogleInnerConfig).callbackURL as string
|
||||||
}
|
}
|
||||||
const publicConfig = await getScopedConfig(db, {
|
const settingsConfig = await configs.getSettingsConfig()
|
||||||
type: ConfigType.SETTINGS,
|
|
||||||
})
|
|
||||||
|
|
||||||
let callbackUrl = `/api/global/auth`
|
let callbackUrl = `/api/global/auth`
|
||||||
if (isMultiTenant()) {
|
if (isMultiTenant()) {
|
||||||
|
@ -37,5 +34,5 @@ export async function ssoCallbackUrl(
|
||||||
}
|
}
|
||||||
callbackUrl += `/${type}/callback`
|
callbackUrl += `/${type}/callback`
|
||||||
|
|
||||||
return `${publicConfig.platformUrl}${callbackUrl}`
|
return `${settingsConfig.platformUrl}${callbackUrl}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,10 @@ export function createQueue<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdown() {
|
export async function shutdown() {
|
||||||
if (QUEUES.length) {
|
if (cleanupInterval) {
|
||||||
clearInterval(cleanupInterval)
|
clearInterval(cleanupInterval)
|
||||||
|
}
|
||||||
|
if (QUEUES.length) {
|
||||||
for (let queue of QUEUES) {
|
for (let queue of QUEUES) {
|
||||||
await queue.close()
|
await queue.close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,13 @@ import {
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import * as tenancy from "../tenancy"
|
import * as tenancy from "../tenancy"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { App, Ctx, TenantResolutionStrategy } from "@budibase/types"
|
import {
|
||||||
|
App,
|
||||||
|
AuditedEventFriendlyName,
|
||||||
|
Ctx,
|
||||||
|
Event,
|
||||||
|
TenantResolutionStrategy,
|
||||||
|
} from "@budibase/types"
|
||||||
import { SetOption } from "cookies"
|
import { SetOption } from "cookies"
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
|
|
||||||
|
@ -217,3 +223,7 @@ export async function getBuildersCount() {
|
||||||
export function timeout(timeMs: number) {
|
export function timeout(timeMs: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, timeMs))
|
return new Promise(resolve => setTimeout(resolve, timeMs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAudited(event: Event) {
|
||||||
|
return !!AuditedEventFriendlyName[event]
|
||||||
|
}
|
||||||
|
|
|
@ -70,6 +70,10 @@ export const useBackups = () => {
|
||||||
return useFeature(Feature.APP_BACKUPS)
|
return useFeature(Feature.APP_BACKUPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useEnforceableSSO = () => {
|
||||||
|
return useFeature(Feature.ENFORCEABLE_SSO)
|
||||||
|
}
|
||||||
|
|
||||||
export const useGroups = () => {
|
export const useGroups = () => {
|
||||||
return useFeature(Feature.USER_GROUPS)
|
return useFeature(Feature.USER_GROUPS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,7 @@ export function oidcConfig(): OIDCInnerConfig {
|
||||||
configUrl: "http://someconfigurl",
|
configUrl: "http://someconfigurl",
|
||||||
clientID: generator.string(),
|
clientID: generator.string(),
|
||||||
clientSecret: generator.string(),
|
clientSecret: generator.string(),
|
||||||
|
scopes: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.12",
|
"@budibase/string-templates": "2.3.18-alpha.13",
|
||||||
"@spectrum-css/accordion": "3.0.24",
|
"@spectrum-css/accordion": "3.0.24",
|
||||||
"@spectrum-css/actionbutton": "1.0.1",
|
"@spectrum-css/actionbutton": "1.0.1",
|
||||||
"@spectrum-css/actiongroup": "1.0.1",
|
"@spectrum-css/actiongroup": "1.0.1",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -58,10 +58,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.3.18-alpha.12",
|
"@budibase/bbui": "2.3.18-alpha.13",
|
||||||
"@budibase/client": "2.3.18-alpha.12",
|
"@budibase/client": "2.3.18-alpha.13",
|
||||||
"@budibase/frontend-core": "2.3.18-alpha.12",
|
"@budibase/frontend-core": "2.3.18-alpha.13",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.12",
|
"@budibase/string-templates": "2.3.18-alpha.13",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
|
|
|
@ -79,67 +79,71 @@
|
||||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||||
<GoogleButton />
|
<GoogleButton />
|
||||||
</FancyForm>
|
</FancyForm>
|
||||||
<Divider />
|
|
||||||
{/if}
|
{/if}
|
||||||
<FancyForm bind:this={form}>
|
{#if !$organisation.isSSOEnforced}
|
||||||
<FancyInput
|
<Divider />
|
||||||
label="Your work email"
|
<FancyForm bind:this={form}>
|
||||||
value={formData.username}
|
<FancyInput
|
||||||
on:change={e => {
|
label="Your work email"
|
||||||
formData = {
|
value={formData.username}
|
||||||
...formData,
|
on:change={e => {
|
||||||
username: e.detail,
|
formData = {
|
||||||
}
|
...formData,
|
||||||
}}
|
username: e.detail,
|
||||||
validate={() => {
|
}
|
||||||
let fieldError = {
|
}}
|
||||||
username: !formData.username
|
validate={() => {
|
||||||
? "Please enter a valid email"
|
let fieldError = {
|
||||||
: undefined,
|
username: !formData.username
|
||||||
}
|
? "Please enter a valid email"
|
||||||
errors = handleError({ ...errors, ...fieldError })
|
: undefined,
|
||||||
}}
|
}
|
||||||
error={errors.username}
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
/>
|
}}
|
||||||
<FancyInput
|
error={errors.username}
|
||||||
label="Password"
|
/>
|
||||||
value={formData.password}
|
<FancyInput
|
||||||
type="password"
|
label="Password"
|
||||||
on:change={e => {
|
value={formData.password}
|
||||||
formData = {
|
type="password"
|
||||||
...formData,
|
on:change={e => {
|
||||||
password: e.detail,
|
formData = {
|
||||||
}
|
...formData,
|
||||||
}}
|
password: e.detail,
|
||||||
validate={() => {
|
}
|
||||||
let fieldError = {
|
}}
|
||||||
password: !formData.password
|
validate={() => {
|
||||||
? "Please enter your password"
|
let fieldError = {
|
||||||
: undefined,
|
password: !formData.password
|
||||||
}
|
? "Please enter your password"
|
||||||
errors = handleError({ ...errors, ...fieldError })
|
: undefined,
|
||||||
}}
|
}
|
||||||
error={errors.password}
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
/>
|
}}
|
||||||
</FancyForm>
|
error={errors.password}
|
||||||
</Layout>
|
/>
|
||||||
<Layout gap="XS" noPadding justifyItems="center">
|
</FancyForm>
|
||||||
<Button
|
{/if}
|
||||||
size="L"
|
|
||||||
cta
|
|
||||||
disabled={Object.keys(errors).length > 0}
|
|
||||||
on:click={login}
|
|
||||||
>
|
|
||||||
Log in to {company}
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="XS" noPadding justifyItems="center">
|
|
||||||
<div class="user-actions">
|
|
||||||
<ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
|
|
||||||
Forgot password?
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
{#if !$organisation.isSSOEnforced}
|
||||||
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
|
<Button
|
||||||
|
size="L"
|
||||||
|
cta
|
||||||
|
disabled={Object.keys(errors).length > 0}
|
||||||
|
on:click={login}
|
||||||
|
>
|
||||||
|
Log in to {company}
|
||||||
|
</Button>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
|
<div class="user-actions">
|
||||||
|
<ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
|
||||||
|
Forgot password?
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if cloud}
|
{#if cloud}
|
||||||
<Body size="xs" textAlign="center">
|
<Body size="xs" textAlign="center">
|
||||||
|
|
|
@ -22,10 +22,11 @@
|
||||||
Tags,
|
Tags,
|
||||||
Icon,
|
Icon,
|
||||||
Helpers,
|
Helpers,
|
||||||
|
Link,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { organisation, admin } from "stores/portal"
|
import { organisation, admin, licensing } from "stores/portal"
|
||||||
|
|
||||||
const ConfigTypes = {
|
const ConfigTypes = {
|
||||||
Google: "google",
|
Google: "google",
|
||||||
|
@ -34,6 +35,8 @@
|
||||||
|
|
||||||
const HasSpacesRegex = /[\\"\s]/
|
const HasSpacesRegex = /[\\"\s]/
|
||||||
|
|
||||||
|
$: enforcedSSO = $organisation.isSSOEnforced
|
||||||
|
|
||||||
// Some older google configs contain a manually specified value - retain the functionality to edit the field
|
// 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
|
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
|
||||||
$: googleCallbackUrl = undefined
|
$: googleCallbackUrl = undefined
|
||||||
|
@ -154,6 +157,11 @@
|
||||||
iconDropdownOptions.unshift({ label: fileName, value: fileName })
|
iconDropdownOptions.unshift({ label: fileName, value: fileName })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleIsSSOEnforced() {
|
||||||
|
const value = $organisation.isSSOEnforced
|
||||||
|
await organisation.save({ isSSOEnforced: !value })
|
||||||
|
}
|
||||||
|
|
||||||
async function save(docs) {
|
async function save(docs) {
|
||||||
let calls = []
|
let calls = []
|
||||||
// Only if the user has provided an image, upload it
|
// Only if the user has provided an image, upload it
|
||||||
|
@ -316,6 +324,49 @@
|
||||||
<Heading size="M">Authentication</Heading>
|
<Heading size="M">Authentication</Heading>
|
||||||
<Body>Add additional authentication methods from the options below</Body>
|
<Body>Add additional authentication methods from the options below</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Heading size="S">Single Sign-On URL</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Use the following link to access your configured identity provider.
|
||||||
|
</Body>
|
||||||
|
<Body size="S">
|
||||||
|
<div class="sso-link">
|
||||||
|
<Link href={$organisation.platformUrl} target="_blank"
|
||||||
|
>{$organisation.platformUrl}</Link
|
||||||
|
>
|
||||||
|
<div class="sso-link-icon">
|
||||||
|
<Icon size="XS" name="LinkOutLight" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<div class="provider-title">
|
||||||
|
<div class="enforce-sso-heading-container">
|
||||||
|
<div class="enforce-sso-title">
|
||||||
|
<Heading size="S">Enforce Single Sign-On</Heading>
|
||||||
|
</div>
|
||||||
|
{#if !$licensing.enforceableSSO}
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Business plan</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if $licensing.enforceableSSO}
|
||||||
|
<Toggle on:change={toggleIsSSOEnforced} bind:value={enforcedSSO} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Body size="S">
|
||||||
|
Require SSO authentication for all users. It is recommended to read the
|
||||||
|
help <Link
|
||||||
|
size="M"
|
||||||
|
href={"https://docs.budibase.com/docs/authentication-and-sso"}
|
||||||
|
>documentation</Link
|
||||||
|
> before enabling this feature.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
{#if providers.google}
|
{#if providers.google}
|
||||||
<Divider />
|
<Divider />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
|
@ -546,7 +597,24 @@
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.sso-link-icon {
|
||||||
|
padding-top: 4px;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
.sso-link {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.enforce-sso-title {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.enforce-sso-heading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
.provider-title {
|
.provider-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = writable({
|
const values = writable({
|
||||||
|
isSSOEnforced: $organisation.isSSOEnforced,
|
||||||
company: $organisation.company,
|
company: $organisation.company,
|
||||||
platformUrl: $organisation.platformUrl,
|
platformUrl: $organisation.platformUrl,
|
||||||
analyticsEnabled: $organisation.analyticsEnabled,
|
analyticsEnabled: $organisation.analyticsEnabled,
|
||||||
|
@ -54,6 +55,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
isSSOEnforced: $values.isSSOEnforced,
|
||||||
company: $values.company ?? "",
|
company: $values.company ?? "",
|
||||||
platformUrl: $values.platformUrl ?? "",
|
platformUrl: $values.platformUrl ?? "",
|
||||||
analyticsEnabled: $values.analyticsEnabled,
|
analyticsEnabled: $values.analyticsEnabled,
|
||||||
|
|
|
@ -63,6 +63,9 @@ export const createLicensingStore = () => {
|
||||||
const environmentVariablesEnabled = license.features.includes(
|
const environmentVariablesEnabled = license.features.includes(
|
||||||
Constants.Features.ENVIRONMENT_VARIABLES
|
Constants.Features.ENVIRONMENT_VARIABLES
|
||||||
)
|
)
|
||||||
|
const enforceableSSO = license.features.includes(
|
||||||
|
Constants.Features.ENFORCEABLE_SSO
|
||||||
|
)
|
||||||
|
|
||||||
const auditLogsEnabled = license.features.includes(
|
const auditLogsEnabled = license.features.includes(
|
||||||
Constants.Features.AUDIT_LOGS
|
Constants.Features.AUDIT_LOGS
|
||||||
|
@ -76,6 +79,7 @@ export const createLicensingStore = () => {
|
||||||
backupsEnabled,
|
backupsEnabled,
|
||||||
environmentVariablesEnabled,
|
environmentVariablesEnabled,
|
||||||
auditLogsEnabled,
|
auditLogsEnabled,
|
||||||
|
enforceableSSO,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,7 @@ const DEFAULT_CONFIG = {
|
||||||
google: undefined,
|
google: undefined,
|
||||||
oidcCallbackUrl: "",
|
oidcCallbackUrl: "",
|
||||||
googleCallbackUrl: "",
|
googleCallbackUrl: "",
|
||||||
|
isSSOEnforced: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOrganisationStore() {
|
export function createOrganisationStore() {
|
||||||
|
@ -19,8 +20,8 @@ export function createOrganisationStore() {
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const tenantId = get(auth).tenantId
|
const tenantId = get(auth).tenantId
|
||||||
const tenant = await API.getTenantConfig(tenantId)
|
const settingsConfigDoc = await API.getTenantConfig(tenantId)
|
||||||
set({ ...DEFAULT_CONFIG, ...tenant.config, _rev: tenant._rev })
|
set({ ...DEFAULT_CONFIG, ...settingsConfigDoc.config })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(config) {
|
async function save(config) {
|
||||||
|
@ -33,7 +34,6 @@ export function createOrganisationStore() {
|
||||||
await API.saveConfig({
|
await API.saveConfig({
|
||||||
type: "settings",
|
type: "settings",
|
||||||
config: { ...get(store), ...config },
|
config: { ...get(store), ...config },
|
||||||
_rev: get(store)._rev,
|
|
||||||
})
|
})
|
||||||
await init()
|
await init()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -26,9 +26,9 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.3.18-alpha.12",
|
"@budibase/backend-core": "2.3.18-alpha.13",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.12",
|
"@budibase/string-templates": "2.3.18-alpha.13",
|
||||||
"@budibase/types": "2.3.18-alpha.12",
|
"@budibase/types": "2.3.18-alpha.13",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.3.18-alpha.12",
|
"@budibase/bbui": "2.3.18-alpha.13",
|
||||||
"@budibase/frontend-core": "2.3.18-alpha.12",
|
"@budibase/frontend-core": "2.3.18-alpha.13",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.12",
|
"@budibase/string-templates": "2.3.18-alpha.13",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.3.18-alpha.12",
|
"@budibase/bbui": "2.3.18-alpha.13",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,7 @@ export const Features = {
|
||||||
BACKUPS: "appBackups",
|
BACKUPS: "appBackups",
|
||||||
ENVIRONMENT_VARIABLES: "environmentVariables",
|
ENVIRONMENT_VARIABLES: "environmentVariables",
|
||||||
AUDIT_LOGS: "auditLogs",
|
AUDIT_LOGS: "auditLogs",
|
||||||
|
ENFORCEABLE_SSO: "enforceableSSO",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Role IDs
|
// Role IDs
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/sdk",
|
"name": "@budibase/sdk",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"description": "Budibase Public API SDK",
|
"description": "Budibase Public API SDK",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -43,11 +43,11 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
"@budibase/backend-core": "2.3.18-alpha.12",
|
"@budibase/backend-core": "2.3.18-alpha.13",
|
||||||
"@budibase/client": "2.3.18-alpha.12",
|
"@budibase/client": "2.3.18-alpha.13",
|
||||||
"@budibase/pro": "2.3.18-alpha.12",
|
"@budibase/pro": "2.3.18-alpha.13",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.12",
|
"@budibase/string-templates": "2.3.18-alpha.13",
|
||||||
"@budibase/types": "2.3.18-alpha.12",
|
"@budibase/types": "2.3.18-alpha.13",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -58,7 +58,7 @@ export async function exportApps(ctx: Ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkHasBeenImported() {
|
async function checkHasBeenImported() {
|
||||||
if (!env.SELF_HOSTED || env.MULTI_TENANCY) {
|
if (!env.SELF_HOSTED) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const apps = await dbCore.getAllApps({ all: true })
|
const apps = await dbCore.getAllApps({ all: true })
|
||||||
|
@ -72,7 +72,7 @@ export async function hasBeenImported(ctx: Ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importApps(ctx: Ctx) {
|
export async function importApps(ctx: Ctx) {
|
||||||
if (!env.SELF_HOSTED || env.MULTI_TENANCY) {
|
if (!env.SELF_HOSTED) {
|
||||||
ctx.throw(400, "Importing only allowed in self hosted environments.")
|
ctx.throw(400, "Importing only allowed in self hosted environments.")
|
||||||
}
|
}
|
||||||
const beenImported = await checkHasBeenImported()
|
const beenImported = await checkHasBeenImported()
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { App } from "@budibase/types"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
import { AppStatus } from "../../../db/utils"
|
import { AppStatus } from "../../../db/utils"
|
||||||
|
@ -5,6 +7,7 @@ import { AppStatus } from "../../../db/utils"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
|
|
||||||
import { wipeDb } from "./utilities/TestFunctions"
|
import { wipeDb } from "./utilities/TestFunctions"
|
||||||
|
import { tenancy } from "@budibase/backend-core"
|
||||||
|
|
||||||
describe("/cloud", () => {
|
describe("/cloud", () => {
|
||||||
let request = setup.getRequest()!
|
let request = setup.getRequest()!
|
||||||
|
@ -12,18 +15,10 @@ describe("/cloud", () => {
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(async () => {
|
||||||
// Importing is only allowed in self hosted environments
|
// Importing is only allowed in self hosted environments
|
||||||
config.modeSelf()
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await config.init()
|
await config.init()
|
||||||
})
|
config.modeSelf()
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
// clear all mocks
|
|
||||||
jest.clearAllMocks()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("import", () => {
|
describe("import", () => {
|
||||||
|
@ -32,30 +27,28 @@ describe("/cloud", () => {
|
||||||
// import will not run
|
// import will not run
|
||||||
await wipeDb()
|
await wipeDb()
|
||||||
|
|
||||||
// get a count of apps before the import
|
|
||||||
const preImportApps = await request
|
|
||||||
.get(`/api/applications?status=${AppStatus.ALL}`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
// Perform the import
|
// Perform the import
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api/cloud/import`)
|
.post(`/api/cloud/import`)
|
||||||
|
.set(config.publicHeaders())
|
||||||
.attach("importFile", "src/api/routes/tests/data/export-test.tar.gz")
|
.attach("importFile", "src/api/routes/tests/data/export-test.tar.gz")
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.message).toEqual("Apps successfully imported.")
|
expect(res.body.message).toEqual("Apps successfully imported.")
|
||||||
|
|
||||||
// get a count of apps after the import
|
// get a count of apps after the import
|
||||||
const postImportApps = await request
|
const postImportApps = await request
|
||||||
.get(`/api/applications?status=${AppStatus.ALL}`)
|
.get(`/api/applications?status=${AppStatus.ALL}`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.publicHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
|
const apps = postImportApps.body as App[]
|
||||||
// There are two apps in the file that was imported so check for this
|
// There are two apps in the file that was imported so check for this
|
||||||
expect(postImportApps.body.length).toEqual(2)
|
expect(apps.length).toEqual(2)
|
||||||
|
// The new tenant id was assigned to the imported apps
|
||||||
|
expect(tenancy.getTenantIDFromAppID(apps[0].appId)).toBe(
|
||||||
|
config.getTenantId()
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -2,7 +2,6 @@ import * as rowController from "../../../controllers/row"
|
||||||
import * as appController from "../../../controllers/application"
|
import * as appController from "../../../controllers/application"
|
||||||
import { AppStatus } from "../../../../db/utils"
|
import { AppStatus } from "../../../../db/utils"
|
||||||
import { roles, tenancy, context } from "@budibase/backend-core"
|
import { roles, tenancy, context } from "@budibase/backend-core"
|
||||||
import { TENANT_ID } from "../../../../tests/utilities/structures"
|
|
||||||
import env from "../../../../environment"
|
import env from "../../../../environment"
|
||||||
import { db } from "@budibase/backend-core"
|
import { db } from "@budibase/backend-core"
|
||||||
import Nano from "@budibase/nano"
|
import Nano from "@budibase/nano"
|
||||||
|
@ -33,7 +32,7 @@ export const getAllTableRows = async (config: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearAllApps = async (
|
export const clearAllApps = async (
|
||||||
tenantId = TENANT_ID,
|
tenantId: string,
|
||||||
exceptions: Array<string> = []
|
exceptions: Array<string> = []
|
||||||
) => {
|
) => {
|
||||||
await tenancy.doInTenant(tenantId, async () => {
|
await tenancy.doInTenant(tenantId, async () => {
|
||||||
|
|
|
@ -11,8 +11,7 @@ import { OAuth2Client } from "google-auth-library"
|
||||||
import { buildExternalTableId } from "./utils"
|
import { buildExternalTableId } from "./utils"
|
||||||
import { DataSourceOperation, FieldTypes } from "../constants"
|
import { DataSourceOperation, FieldTypes } from "../constants"
|
||||||
import { GoogleSpreadsheet } from "google-spreadsheet"
|
import { GoogleSpreadsheet } from "google-spreadsheet"
|
||||||
import env from "../environment"
|
import { configs, HTTPError } from "@budibase/backend-core"
|
||||||
import { tenancy, db as dbCore, constants } from "@budibase/backend-core"
|
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
|
|
||||||
interface GoogleSheetsConfig {
|
interface GoogleSheetsConfig {
|
||||||
|
@ -173,16 +172,9 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
async connect() {
|
async connect() {
|
||||||
try {
|
try {
|
||||||
// Initialise oAuth client
|
// Initialise oAuth client
|
||||||
const db = tenancy.getGlobalDB()
|
let googleConfig = await configs.getGoogleConfig()
|
||||||
let googleConfig = await dbCore.getScopedConfig(db, {
|
|
||||||
type: constants.Config.GOOGLE,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!googleConfig) {
|
if (!googleConfig) {
|
||||||
googleConfig = {
|
throw new HTTPError("Google config not found", 400)
|
||||||
clientID: env.GOOGLE_CLIENT_ID,
|
|
||||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const oauthClient = new OAuth2Client({
|
const oauthClient = new OAuth2Client({
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { events, db as dbUtils } from "@budibase/backend-core"
|
import {
|
||||||
|
events,
|
||||||
|
DocumentType,
|
||||||
|
SEPARATOR,
|
||||||
|
UNICODE_MAX,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
isSMTPConfig,
|
isSMTPConfig,
|
||||||
|
@ -9,15 +14,16 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import env from "./../../../../environment"
|
import env from "./../../../../environment"
|
||||||
|
|
||||||
|
export const getConfigParams = () => {
|
||||||
|
return {
|
||||||
|
include_docs: true,
|
||||||
|
startkey: `${DocumentType.CONFIG}${SEPARATOR}`,
|
||||||
|
endkey: `${DocumentType.CONFIG}${SEPARATOR}${UNICODE_MAX}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getConfigs = async (globalDb: any): Promise<Config[]> => {
|
const getConfigs = async (globalDb: any): Promise<Config[]> => {
|
||||||
const response = await globalDb.allDocs(
|
const response = await globalDb.allDocs(getConfigParams())
|
||||||
dbUtils.getConfigParams(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
include_docs: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return response.rows.map((row: any) => row.doc)
|
return response.rows.map((row: any) => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,8 @@ jest.mock("@budibase/backend-core", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const { tenancy, db: dbCore } = require("@budibase/backend-core")
|
const { context, db: dbCore } = require("@budibase/backend-core")
|
||||||
const TestConfig = require("../../../tests/utilities/TestConfiguration")
|
const TestConfig = require("../../../tests/utilities/TestConfiguration")
|
||||||
const { TENANT_ID } = require("../../../tests/utilities/structures")
|
|
||||||
|
|
||||||
// mock email view creation
|
// mock email view creation
|
||||||
|
|
||||||
|
@ -26,8 +25,8 @@ describe("run", () => {
|
||||||
afterAll(config.end)
|
afterAll(config.end)
|
||||||
|
|
||||||
it("runs successfully", async () => {
|
it("runs successfully", async () => {
|
||||||
await tenancy.doInTenant(TENANT_ID, async () => {
|
await config.doInTenant(async () => {
|
||||||
const globalDb = tenancy.getGlobalDB()
|
const globalDb = context.getGlobalDB()
|
||||||
await migration.run(globalDb)
|
await migration.run(globalDb)
|
||||||
expect(dbCore.createNewUserEmailView).toHaveBeenCalledTimes(1)
|
expect(dbCore.createNewUserEmailView).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Mimic configs test configuration from worker, creation configs directly in database
|
// Mimic configs test configuration from worker, creation configs directly in database
|
||||||
|
|
||||||
import * as structures from "./structures"
|
import * as structures from "./structures"
|
||||||
import { db } from "@budibase/backend-core"
|
import { configs } from "@budibase/backend-core"
|
||||||
import { Config } from "@budibase/types"
|
import { Config } from "@budibase/types"
|
||||||
|
|
||||||
export const saveSettingsConfig = async (globalDb: any) => {
|
export const saveSettingsConfig = async (globalDb: any) => {
|
||||||
|
@ -25,7 +25,7 @@ export const saveSmtpConfig = async (globalDb: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveConfig = async (config: Config, globalDb: any) => {
|
const saveConfig = async (config: Config, globalDb: any) => {
|
||||||
config._id = db.generateConfigID({ type: config.type })
|
config._id = configs.generateConfigID(config.type)
|
||||||
|
|
||||||
let response
|
let response
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -20,6 +20,7 @@ export const oidc = (conf?: OIDCConfig): OIDCConfig => {
|
||||||
name: "Active Directory",
|
name: "Active Directory",
|
||||||
uuid: utils.newid(),
|
uuid: utils.newid(),
|
||||||
activated: true,
|
activated: true,
|
||||||
|
scopes: [],
|
||||||
...conf,
|
...conf,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -17,7 +17,6 @@ import * as pro from "@budibase/pro"
|
||||||
import * as api from "./api"
|
import * as api from "./api"
|
||||||
import sdk from "./sdk"
|
import sdk from "./sdk"
|
||||||
const pino = require("koa-pino-logger")
|
const pino = require("koa-pino-logger")
|
||||||
import { sdk as proSdk } from "@budibase/pro"
|
|
||||||
|
|
||||||
let STARTUP_RAN = false
|
let STARTUP_RAN = false
|
||||||
|
|
||||||
|
@ -127,7 +126,7 @@ export async function startup(app?: any, server?: any) {
|
||||||
let queuePromises = []
|
let queuePromises = []
|
||||||
// configure events to use the pro audit log write
|
// configure events to use the pro audit log write
|
||||||
// can't integrate directly into backend-core due to cyclic issues
|
// can't integrate directly into backend-core due to cyclic issues
|
||||||
queuePromises.push(events.processors.init(proSdk.auditLogs.write))
|
queuePromises.push(events.processors.init(pro.sdk.auditLogs.write))
|
||||||
queuePromises.push(automations.init())
|
queuePromises.push(automations.init())
|
||||||
queuePromises.push(initPro())
|
queuePromises.push(initPro())
|
||||||
if (app) {
|
if (app) {
|
||||||
|
|
|
@ -8,3 +8,4 @@ process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
|
||||||
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
|
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
|
||||||
process.env.ENABLE_4XX_HTTP_LOGGING = "0"
|
process.env.ENABLE_4XX_HTTP_LOGGING = "0"
|
||||||
process.env.MOCK_REDIS = "1"
|
process.env.MOCK_REDIS = "1"
|
||||||
|
process.env.PLATFORM_URL = "http://localhost:10000"
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {
|
||||||
basicScreen,
|
basicScreen,
|
||||||
basicLayout,
|
basicLayout,
|
||||||
basicWebhook,
|
basicWebhook,
|
||||||
TENANT_ID,
|
|
||||||
} from "./structures"
|
} from "./structures"
|
||||||
import {
|
import {
|
||||||
constants,
|
constants,
|
||||||
|
@ -41,8 +40,8 @@ import { generateUserMetadataID } from "../../db/utils"
|
||||||
import { startup } from "../../startup"
|
import { startup } from "../../startup"
|
||||||
import supertest from "supertest"
|
import supertest from "supertest"
|
||||||
import {
|
import {
|
||||||
|
App,
|
||||||
AuthToken,
|
AuthToken,
|
||||||
Database,
|
|
||||||
Datasource,
|
Datasource,
|
||||||
Row,
|
Row,
|
||||||
SourceName,
|
SourceName,
|
||||||
|
@ -63,7 +62,7 @@ class TestConfiguration {
|
||||||
started: boolean
|
started: boolean
|
||||||
appId: string | null
|
appId: string | null
|
||||||
allApps: any[]
|
allApps: any[]
|
||||||
app: any
|
app?: App
|
||||||
prodApp: any
|
prodApp: any
|
||||||
prodAppId: any
|
prodAppId: any
|
||||||
user: any
|
user: any
|
||||||
|
@ -73,7 +72,7 @@ class TestConfiguration {
|
||||||
linkedTable: any
|
linkedTable: any
|
||||||
automation: any
|
automation: any
|
||||||
datasource: any
|
datasource: any
|
||||||
tenantId: string | null
|
tenantId?: string
|
||||||
defaultUserValues: DefaultUserValues
|
defaultUserValues: DefaultUserValues
|
||||||
|
|
||||||
constructor(openServer = true) {
|
constructor(openServer = true) {
|
||||||
|
@ -89,7 +88,6 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
this.appId = null
|
this.appId = null
|
||||||
this.allApps = []
|
this.allApps = []
|
||||||
this.tenantId = null
|
|
||||||
this.defaultUserValues = this.populateDefaultUserValues()
|
this.defaultUserValues = this.populateDefaultUserValues()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,19 +152,10 @@ class TestConfiguration {
|
||||||
|
|
||||||
// use a new id as the name to avoid name collisions
|
// use a new id as the name to avoid name collisions
|
||||||
async init(appName = newid()) {
|
async init(appName = newid()) {
|
||||||
this.defaultUserValues = this.populateDefaultUserValues()
|
|
||||||
if (context.isMultiTenant()) {
|
|
||||||
this.tenantId = structures.tenant.id()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.started) {
|
if (!this.started) {
|
||||||
await startup()
|
await startup()
|
||||||
}
|
}
|
||||||
this.user = await this.globalUser()
|
return this.newTenant(appName)
|
||||||
this.globalUserId = this.user._id
|
|
||||||
this.userMetadataId = generateUserMetadataID(this.globalUserId)
|
|
||||||
|
|
||||||
return this.createApp(appName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
|
@ -182,24 +171,22 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MODES
|
// MODES
|
||||||
#setMultiTenancy = (value: boolean) => {
|
setMultiTenancy = (value: boolean) => {
|
||||||
env._set("MULTI_TENANCY", value)
|
env._set("MULTI_TENANCY", value)
|
||||||
coreEnv._set("MULTI_TENANCY", value)
|
coreEnv._set("MULTI_TENANCY", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
#setSelfHosted = (value: boolean) => {
|
setSelfHosted = (value: boolean) => {
|
||||||
env._set("SELF_HOSTED", value)
|
env._set("SELF_HOSTED", value)
|
||||||
coreEnv._set("SELF_HOSTED", value)
|
coreEnv._set("SELF_HOSTED", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
modeCloud = () => {
|
modeCloud = () => {
|
||||||
this.#setSelfHosted(false)
|
this.setSelfHosted(false)
|
||||||
this.#setMultiTenancy(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modeSelf = () => {
|
modeSelf = () => {
|
||||||
this.#setSelfHosted(true)
|
this.setSelfHosted(true)
|
||||||
this.#setMultiTenancy(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UTILS
|
// UTILS
|
||||||
|
@ -354,6 +341,8 @@ class TestConfiguration {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HEADERS
|
||||||
|
|
||||||
defaultHeaders(extras = {}) {
|
defaultHeaders(extras = {}) {
|
||||||
const tenantId = this.getTenantId()
|
const tenantId = this.getTenantId()
|
||||||
const authObj: AuthToken = {
|
const authObj: AuthToken = {
|
||||||
|
@ -374,6 +363,7 @@ class TestConfiguration {
|
||||||
`${constants.Cookie.CurrentApp}=${appToken}`,
|
`${constants.Cookie.CurrentApp}=${appToken}`,
|
||||||
],
|
],
|
||||||
[constants.Header.CSRF_TOKEN]: this.defaultUserValues.csrfToken,
|
[constants.Header.CSRF_TOKEN]: this.defaultUserValues.csrfToken,
|
||||||
|
Host: this.tenantHost(),
|
||||||
...extras,
|
...extras,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,10 +373,6 @@ class TestConfiguration {
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
getTenantId() {
|
|
||||||
return this.tenantId || TENANT_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
publicHeaders({ prodApp = true } = {}) {
|
publicHeaders({ prodApp = true } = {}) {
|
||||||
const appId = prodApp ? this.prodAppId : this.appId
|
const appId = prodApp ? this.prodAppId : this.appId
|
||||||
|
|
||||||
|
@ -397,9 +383,7 @@ class TestConfiguration {
|
||||||
headers[constants.Header.APP_ID] = appId
|
headers[constants.Header.APP_ID] = appId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tenantId) {
|
headers[constants.Header.TENANT_ID] = this.getTenantId()
|
||||||
headers[constants.Header.TENANT_ID] = this.tenantId
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
@ -413,6 +397,34 @@ class TestConfiguration {
|
||||||
return this.login({ email, roleId, builder, prodApp })
|
return this.login({ email, roleId, builder, prodApp })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TENANCY
|
||||||
|
|
||||||
|
tenantHost() {
|
||||||
|
const tenantId = this.getTenantId()
|
||||||
|
const platformHost = new URL(coreEnv.PLATFORM_URL).host.split(":")[0]
|
||||||
|
return `${tenantId}.${platformHost}`
|
||||||
|
}
|
||||||
|
|
||||||
|
getTenantId() {
|
||||||
|
if (!this.tenantId) {
|
||||||
|
throw new Error("no test tenant id - init has not been called")
|
||||||
|
}
|
||||||
|
return this.tenantId
|
||||||
|
}
|
||||||
|
|
||||||
|
async newTenant(appName = newid()): Promise<App> {
|
||||||
|
this.defaultUserValues = this.populateDefaultUserValues()
|
||||||
|
this.tenantId = structures.tenant.id()
|
||||||
|
this.user = await this.globalUser()
|
||||||
|
this.globalUserId = this.user._id
|
||||||
|
this.userMetadataId = generateUserMetadataID(this.globalUserId)
|
||||||
|
return this.createApp(appName)
|
||||||
|
}
|
||||||
|
|
||||||
|
doInTenant(task: any) {
|
||||||
|
return context.doInTenant(this.getTenantId(), task)
|
||||||
|
}
|
||||||
|
|
||||||
// API
|
// API
|
||||||
|
|
||||||
async generateApiKey(userId = this.defaultUserValues.globalUserId) {
|
async generateApiKey(userId = this.defaultUserValues.globalUserId) {
|
||||||
|
@ -432,7 +444,7 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
// APP
|
// APP
|
||||||
async createApp(appName: string) {
|
async createApp(appName: string): Promise<App> {
|
||||||
// create dev app
|
// create dev app
|
||||||
// clear any old app
|
// clear any old app
|
||||||
this.appId = null
|
this.appId = null
|
||||||
|
@ -442,7 +454,7 @@ class TestConfiguration {
|
||||||
null,
|
null,
|
||||||
controllers.app.create
|
controllers.app.create
|
||||||
)
|
)
|
||||||
this.appId = this.app.appId
|
this.appId = this.app?.appId!
|
||||||
})
|
})
|
||||||
return await context.doInAppContext(this.appId, async () => {
|
return await context.doInAppContext(this.appId, async () => {
|
||||||
// create production app
|
// create production app
|
||||||
|
|
|
@ -13,8 +13,6 @@ import {
|
||||||
|
|
||||||
const { v4: uuidv4 } = require("uuid")
|
const { v4: uuidv4 } = require("uuid")
|
||||||
|
|
||||||
export const TENANT_ID = "default"
|
|
||||||
|
|
||||||
export function basicTable() {
|
export function basicTable() {
|
||||||
return {
|
return {
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
|
|
|
@ -1278,14 +1278,14 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/backend-core@2.3.18-alpha.12":
|
"@budibase/backend-core@2.3.18-alpha.13":
|
||||||
version "2.3.18-alpha.12"
|
version "2.3.18-alpha.13"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.12.tgz#ad1b16be64b78b596af2b5f75647c32e8f6f101a"
|
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.13.tgz#b797d7a4d30ff7f21e473334edb4086f5818830e"
|
||||||
integrity sha512-E1NEO+/sNkkRqn/xk9XQmFBO9/dl27w9EB0QGztti/16JV9NgxyDQCJIdGwlD08s1y/lUwOKk0TkSZJs+CTYDw==
|
integrity sha512-c6d0xCRgLlPeX1euAoQuoDOkMkDGQy/miBx/Z8xyU9bzDDTNqBTogxpxsNf8DdZG7EMJhsJlCUvb26Onz7/50A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/nano" "10.1.1"
|
"@budibase/nano" "10.1.1"
|
||||||
"@budibase/pouchdb-replication-stream" "1.2.10"
|
"@budibase/pouchdb-replication-stream" "1.2.10"
|
||||||
"@budibase/types" "2.3.18-alpha.12"
|
"@budibase/types" "2.3.18-alpha.13"
|
||||||
"@shopify/jest-koa-mocks" "5.0.1"
|
"@shopify/jest-koa-mocks" "5.0.1"
|
||||||
"@techpass/passport-openidconnect" "0.3.2"
|
"@techpass/passport-openidconnect" "0.3.2"
|
||||||
aws-cloudfront-sign "2.2.0"
|
aws-cloudfront-sign "2.2.0"
|
||||||
|
@ -1392,13 +1392,13 @@
|
||||||
pouchdb-promise "^6.0.4"
|
pouchdb-promise "^6.0.4"
|
||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
|
|
||||||
"@budibase/pro@2.3.18-alpha.12":
|
"@budibase/pro@2.3.18-alpha.13":
|
||||||
version "2.3.18-alpha.12"
|
version "2.3.18-alpha.13"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.12.tgz#be552b3a9f5850e746081540d6586aae69147bec"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.13.tgz#9f3dd0339184a58609c726f3f2b4580cf802dc78"
|
||||||
integrity sha512-M3b0njzSi47KH6uaQfYPoA2KWrjPiwcU3ONyaVWXHIktVrIKtYaFwOLBr/dmWGfMrL2297SSqg7V4DTaLyAhnw==
|
integrity sha512-pRroVVFGITFsFtzzH6LgzuaUBKldvFQxTgO7K6dYjjE7xwTCnZCg0E4L2Ew/JIY39UN2WEb5bwdt3+pqPH7Dmg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/backend-core" "2.3.18-alpha.12"
|
"@budibase/backend-core" "2.3.18-alpha.13"
|
||||||
"@budibase/types" "2.3.18-alpha.12"
|
"@budibase/types" "2.3.18-alpha.13"
|
||||||
"@koa/router" "8.0.8"
|
"@koa/router" "8.0.8"
|
||||||
bull "4.10.1"
|
bull "4.10.1"
|
||||||
joi "17.6.0"
|
joi "17.6.0"
|
||||||
|
@ -1424,10 +1424,10 @@
|
||||||
svelte-apexcharts "^1.0.2"
|
svelte-apexcharts "^1.0.2"
|
||||||
svelte-flatpickr "^3.1.0"
|
svelte-flatpickr "^3.1.0"
|
||||||
|
|
||||||
"@budibase/types@2.3.18-alpha.12":
|
"@budibase/types@2.3.18-alpha.13":
|
||||||
version "2.3.18-alpha.12"
|
version "2.3.18-alpha.13"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.12.tgz#a63eb978ccc7e55c209b3e9d71f9aecf7facc0d1"
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.13.tgz#e80cbf79249ed5cf8fe86e294d0b1e25e0a839ee"
|
||||||
integrity sha512-27o2BmI/HXIR3frZ8FtqHgAe1hd8jPIzgPaEhKrQiYJ/opUVccqupx9ld75Hyk9E6cdXu0UF0/+LxPpUmMugag==
|
integrity sha512-fmgpwMMGkbPOObmFnZZH8iKelycmhvBydGQuPEmIk1c449ysfiKF2Strnca6MaY1XtskfRz/nNdWGdGS1HhmFw==
|
||||||
|
|
||||||
"@bull-board/api@3.7.0":
|
"@bull-board/api@3.7.0":
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/types",
|
"name": "@budibase/types",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"description": "Budibase types",
|
"description": "Budibase types",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
23
packages/types/src/api/web/global/configs.ts
Normal file
23
packages/types/src/api/web/global/configs.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { SettingsConfig, SettingsInnerConfig } from "../../../documents"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings that aren't stored in the database - enriched at runtime.
|
||||||
|
*/
|
||||||
|
export interface PublicSettingsInnerConfig extends SettingsInnerConfig {
|
||||||
|
google: boolean
|
||||||
|
oidc: boolean
|
||||||
|
oidcCallbackUrl: string
|
||||||
|
googleCallbackUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPublicSettingsResponse extends SettingsConfig {
|
||||||
|
config: PublicSettingsInnerConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicOIDCConfig {
|
||||||
|
logo?: string
|
||||||
|
name?: string
|
||||||
|
uuid?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetPublicOIDCConfigResponse = PublicOIDCConfig[]
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./environmentVariables"
|
export * from "./environmentVariables"
|
||||||
export * from "./auditLogs"
|
export * from "./auditLogs"
|
||||||
export * from "./events"
|
export * from "./events"
|
||||||
|
export * from "./configs"
|
||||||
|
|
|
@ -5,32 +5,45 @@ export interface Config extends Document {
|
||||||
config: any
|
config: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SMTPConfig extends Config {
|
export interface SMTPInnerConfig {
|
||||||
config: {
|
port: number
|
||||||
port: number
|
host: string
|
||||||
host: string
|
from: string
|
||||||
from: string
|
subject?: string
|
||||||
subject: string
|
secure: boolean
|
||||||
secure: boolean
|
auth?: {
|
||||||
|
user: string
|
||||||
|
pass: string
|
||||||
}
|
}
|
||||||
|
connectionTimeout?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SMTPConfig extends Config {
|
||||||
|
config: SMTPInnerConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingsInnerConfig {
|
||||||
|
platformUrl?: string
|
||||||
|
company?: string
|
||||||
|
logoUrl?: string // Populated on read
|
||||||
|
logoUrlEtag?: string
|
||||||
|
uniqueTenantId?: string
|
||||||
|
analyticsEnabled?: boolean
|
||||||
|
isSSOEnforced?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsConfig extends Config {
|
export interface SettingsConfig extends Config {
|
||||||
config: {
|
config: SettingsInnerConfig
|
||||||
company: string
|
|
||||||
// Populated on read
|
|
||||||
logoUrl?: string
|
|
||||||
logoUrlEtag?: boolean
|
|
||||||
platformUrl: string
|
|
||||||
uniqueTenantId?: string
|
|
||||||
analyticsEnabled?: boolean
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoogleInnerConfig {
|
export interface GoogleInnerConfig {
|
||||||
clientID: string
|
clientID: string
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
activated: boolean
|
activated: boolean
|
||||||
|
/**
|
||||||
|
* @deprecated read only
|
||||||
|
*/
|
||||||
|
callbackURL?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoogleConfig extends Config {
|
export interface GoogleConfig extends Config {
|
||||||
|
@ -55,6 +68,7 @@ export interface OIDCInnerConfig {
|
||||||
name: string
|
name: string
|
||||||
uuid: string
|
uuid: string
|
||||||
activated: boolean
|
activated: boolean
|
||||||
|
scopes: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OIDCConfig extends Config {
|
export interface OIDCConfig extends Config {
|
||||||
|
|
|
@ -35,8 +35,8 @@ export enum IdentityType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HostInfo {
|
export interface HostInfo {
|
||||||
ipAddress: string
|
ipAddress?: string
|
||||||
userAgent: string
|
userAgent?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Identity {
|
export interface Identity {
|
||||||
|
|
|
@ -3,4 +3,5 @@ export enum Feature {
|
||||||
APP_BACKUPS = "appBackups",
|
APP_BACKUPS = "appBackups",
|
||||||
ENVIRONMENT_VARIABLES = "environmentVariables",
|
ENVIRONMENT_VARIABLES = "environmentVariables",
|
||||||
AUDIT_LOGS = "auditLogs",
|
AUDIT_LOGS = "auditLogs",
|
||||||
|
ENFORCEABLE_SSO = "enforceableSSO",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.3.18-alpha.12",
|
"version": "2.3.18-alpha.13",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -36,10 +36,10 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.3.18-alpha.12",
|
"@budibase/backend-core": "2.3.18-alpha.13",
|
||||||
"@budibase/pro": "2.3.18-alpha.12",
|
"@budibase/pro": "2.3.18-alpha.13",
|
||||||
"@budibase/string-templates": "2.3.18-alpha.12",
|
"@budibase/string-templates": "2.3.18-alpha.13",
|
||||||
"@budibase/types": "2.3.18-alpha.12",
|
"@budibase/types": "2.3.18-alpha.13",
|
||||||
"@koa/router": "8.0.8",
|
"@koa/router": "8.0.8",
|
||||||
"@sentry/node": "6.17.7",
|
"@sentry/node": "6.17.7",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
|
|
|
@ -2,10 +2,9 @@ import {
|
||||||
auth as authCore,
|
auth as authCore,
|
||||||
constants,
|
constants,
|
||||||
context,
|
context,
|
||||||
db as dbCore,
|
|
||||||
events,
|
events,
|
||||||
tenancy,
|
|
||||||
utils as utilsCore,
|
utils as utilsCore,
|
||||||
|
configs,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
ConfigType,
|
ConfigType,
|
||||||
|
@ -15,6 +14,7 @@ import {
|
||||||
SSOUser,
|
SSOUser,
|
||||||
PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
PasswordResetUpdateRequest,
|
PasswordResetUpdateRequest,
|
||||||
|
GoogleInnerConfig,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ export const login = async (ctx: Ctx<LoginRequest>, next: any) => {
|
||||||
const email = ctx.request.body.username
|
const email = ctx.request.body.username
|
||||||
|
|
||||||
const user = await userSdk.getUserByEmail(email)
|
const user = await userSdk.getUserByEmail(email)
|
||||||
if (user && (await userSdk.isPreventSSOPasswords(user))) {
|
if (user && (await userSdk.isPreventPasswordActions(user))) {
|
||||||
ctx.throw(400, "SSO user cannot login using password")
|
ctx.throw(400, "Password login is disabled for this user")
|
||||||
}
|
}
|
||||||
|
|
||||||
return passport.authenticate(
|
return passport.authenticate(
|
||||||
|
@ -163,8 +163,8 @@ export const datasourceAuth = async (ctx: any, next: any) => {
|
||||||
|
|
||||||
// GOOGLE SSO
|
// GOOGLE SSO
|
||||||
|
|
||||||
export async function googleCallbackUrl(config?: { callbackURL?: string }) {
|
export async function googleCallbackUrl(config?: GoogleInnerConfig) {
|
||||||
return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.GOOGLE)
|
return ssoCallbackUrl(ConfigType.GOOGLE, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -172,12 +172,10 @@ export async function googleCallbackUrl(config?: { callbackURL?: string }) {
|
||||||
* On a successful login, you will be redirected to the googleAuth callback route.
|
* On a successful login, you will be redirected to the googleAuth callback route.
|
||||||
*/
|
*/
|
||||||
export const googlePreAuth = async (ctx: any, next: any) => {
|
export const googlePreAuth = async (ctx: any, next: any) => {
|
||||||
const db = tenancy.getGlobalDB()
|
const config = await configs.getGoogleConfig()
|
||||||
|
if (!config) {
|
||||||
const config = await dbCore.getScopedConfig(db, {
|
return ctx.throw(400, "Google config not found")
|
||||||
type: ConfigType.GOOGLE,
|
}
|
||||||
workspace: ctx.query.workspace,
|
|
||||||
})
|
|
||||||
let callbackUrl = await googleCallbackUrl(config)
|
let callbackUrl = await googleCallbackUrl(config)
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
config,
|
config,
|
||||||
|
@ -193,12 +191,10 @@ export const googlePreAuth = async (ctx: any, next: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const googleCallback = async (ctx: any, next: any) => {
|
export const googleCallback = async (ctx: any, next: any) => {
|
||||||
const db = tenancy.getGlobalDB()
|
const config = await configs.getGoogleConfig()
|
||||||
|
if (!config) {
|
||||||
const config = await dbCore.getScopedConfig(db, {
|
return ctx.throw(400, "Google config not found")
|
||||||
type: ConfigType.GOOGLE,
|
}
|
||||||
workspace: ctx.query.workspace,
|
|
||||||
})
|
|
||||||
const callbackUrl = await googleCallbackUrl(config)
|
const callbackUrl = await googleCallbackUrl(config)
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
config,
|
config,
|
||||||
|
@ -221,25 +217,20 @@ export const googleCallback = async (ctx: any, next: any) => {
|
||||||
|
|
||||||
// OIDC SSO
|
// OIDC SSO
|
||||||
|
|
||||||
export async function oidcCallbackUrl(config?: { callbackURL?: string }) {
|
export async function oidcCallbackUrl() {
|
||||||
return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.OIDC)
|
return ssoCallbackUrl(ConfigType.OIDC)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const oidcStrategyFactory = async (ctx: any, configId: any) => {
|
export const oidcStrategyFactory = async (ctx: any, configId: any) => {
|
||||||
const db = tenancy.getGlobalDB()
|
const config = await configs.getOIDCConfig()
|
||||||
const config = await dbCore.getScopedConfig(db, {
|
if (!config) {
|
||||||
type: ConfigType.OIDC,
|
return ctx.throw(400, "OIDC config not found")
|
||||||
group: ctx.query.group,
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
let callbackUrl = await oidcCallbackUrl()
|
||||||
let callbackUrl = await oidcCallbackUrl(chosenConfig)
|
|
||||||
|
|
||||||
//Remote Config
|
//Remote Config
|
||||||
const enrichedConfig = await oidc.fetchStrategyConfig(
|
const enrichedConfig = await oidc.fetchStrategyConfig(config, callbackUrl)
|
||||||
chosenConfig,
|
|
||||||
callbackUrl
|
|
||||||
)
|
|
||||||
return oidc.strategyFactory(enrichedConfig, userSdk.save)
|
return oidc.strategyFactory(enrichedConfig, userSdk.save)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,23 +238,23 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => {
|
||||||
* The initial call that OIDC authentication makes to take you to the configured OIDC login screen.
|
* The initial call that OIDC authentication makes to take you to the configured OIDC login screen.
|
||||||
* On a successful login, you will be redirected to the oidcAuth callback route.
|
* On a successful login, you will be redirected to the oidcAuth callback route.
|
||||||
*/
|
*/
|
||||||
export const oidcPreAuth = async (ctx: any, next: any) => {
|
export const oidcPreAuth = async (ctx: Ctx, next: any) => {
|
||||||
const { configId } = ctx.params
|
const { configId } = ctx.params
|
||||||
|
if (!configId) {
|
||||||
|
ctx.throw(400, "OIDC config id is required")
|
||||||
|
}
|
||||||
const strategy = await oidcStrategyFactory(ctx, configId)
|
const strategy = await oidcStrategyFactory(ctx, configId)
|
||||||
|
|
||||||
setCookie(ctx, configId, Cookie.OIDC_CONFIG)
|
setCookie(ctx, configId, Cookie.OIDC_CONFIG)
|
||||||
|
|
||||||
const db = tenancy.getGlobalDB()
|
const config = await configs.getOIDCConfigById(configId)
|
||||||
const config = await dbCore.getScopedConfig(db, {
|
if (!config) {
|
||||||
type: ConfigType.OIDC,
|
return ctx.throw(400, "OIDC config not found")
|
||||||
group: ctx.query.group,
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
|
||||||
|
|
||||||
let authScopes =
|
let authScopes =
|
||||||
chosenConfig.scopes?.length > 0
|
config.scopes?.length > 0
|
||||||
? chosenConfig.scopes
|
? config.scopes
|
||||||
: ["profile", "email", "offline_access"]
|
: ["profile", "email", "offline_access"]
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
return passport.authenticate(strategy, {
|
||||||
|
|
|
@ -2,38 +2,32 @@ import * as email from "../../../utilities/email"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { googleCallbackUrl, oidcCallbackUrl } from "./auth"
|
import { googleCallbackUrl, oidcCallbackUrl } from "./auth"
|
||||||
import {
|
import {
|
||||||
events,
|
|
||||||
cache,
|
cache,
|
||||||
objectStore,
|
configs,
|
||||||
tenancy,
|
|
||||||
db as dbCore,
|
db as dbCore,
|
||||||
env as coreEnv,
|
env as coreEnv,
|
||||||
|
events,
|
||||||
|
objectStore,
|
||||||
|
tenancy,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { checkAnyUserExists } from "../../../utilities/users"
|
import { checkAnyUserExists } from "../../../utilities/users"
|
||||||
import {
|
import {
|
||||||
Database,
|
Config,
|
||||||
Config as ConfigDoc,
|
|
||||||
ConfigType,
|
ConfigType,
|
||||||
SSOType,
|
Ctx,
|
||||||
GoogleConfig,
|
GetPublicOIDCConfigResponse,
|
||||||
OIDCConfig,
|
GetPublicSettingsResponse,
|
||||||
SettingsConfig,
|
|
||||||
isGoogleConfig,
|
isGoogleConfig,
|
||||||
isOIDCConfig,
|
isOIDCConfig,
|
||||||
isSettingsConfig,
|
isSettingsConfig,
|
||||||
isSMTPConfig,
|
isSMTPConfig,
|
||||||
Ctx,
|
|
||||||
UserCtx,
|
UserCtx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import * as pro from "@budibase/pro"
|
||||||
|
|
||||||
const getEventFns = async (db: Database, config: ConfigDoc) => {
|
const getEventFns = async (config: Config, existing?: Config) => {
|
||||||
const fns = []
|
const fns = []
|
||||||
|
|
||||||
let existing
|
|
||||||
if (config._id) {
|
|
||||||
existing = await db.get(config._id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
if (isSMTPConfig(config)) {
|
if (isSMTPConfig(config)) {
|
||||||
fns.push(events.email.SMTPCreated)
|
fns.push(events.email.SMTPCreated)
|
||||||
|
@ -125,21 +119,21 @@ const getEventFns = async (db: Database, config: ConfigDoc) => {
|
||||||
return fns
|
return fns
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: UserCtx) {
|
export async function save(ctx: UserCtx<Config>) {
|
||||||
const db = tenancy.getGlobalDB()
|
const body = ctx.request.body
|
||||||
const { type, workspace, user, config } = ctx.request.body
|
const type = body.type
|
||||||
let eventFns = await getEventFns(db, ctx.request.body)
|
const config = body.config
|
||||||
// Config does not exist yet
|
|
||||||
if (!ctx.request.body._id) {
|
const existingConfig = await configs.getConfig(type)
|
||||||
ctx.request.body._id = dbCore.generateConfigID({
|
let eventFns = await getEventFns(ctx.request.body, existingConfig)
|
||||||
type,
|
|
||||||
workspace,
|
if (existingConfig) {
|
||||||
user,
|
body._rev = existingConfig._rev
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// verify the configuration
|
// verify the configuration
|
||||||
switch (type) {
|
switch (config.type) {
|
||||||
case ConfigType.SMTP:
|
case ConfigType.SMTP:
|
||||||
await email.verifyConfig(config)
|
await email.verifyConfig(config)
|
||||||
break
|
break
|
||||||
|
@ -149,7 +143,8 @@ export async function save(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await db.put(ctx.request.body)
|
body._id = configs.generateConfigID(type)
|
||||||
|
const response = await configs.save(body)
|
||||||
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
||||||
await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED)
|
await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED)
|
||||||
|
|
||||||
|
@ -167,44 +162,11 @@ export async function save(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
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 {
|
try {
|
||||||
// Find the config with the most granular scope based on context
|
// Find the config with the most granular scope based on context
|
||||||
const scopedConfig = await dbCore.getScopedFullConfig(db, {
|
const type = ctx.params.type
|
||||||
type: ctx.params.type,
|
const scopedConfig = await configs.getConfig(type)
|
||||||
user: userId,
|
|
||||||
workspace: workspaceId,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (scopedConfig) {
|
if (scopedConfig) {
|
||||||
ctx.body = scopedConfig
|
ctx.body = scopedConfig
|
||||||
|
@ -217,85 +179,70 @@ export async function find(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function publicOidc(ctx: Ctx) {
|
export async function publicOidc(ctx: Ctx<void, GetPublicOIDCConfigResponse>) {
|
||||||
const db = tenancy.getGlobalDB()
|
|
||||||
try {
|
try {
|
||||||
// Find the config with the most granular scope based on context
|
// Find the config with the most granular scope based on context
|
||||||
const oidcConfig: OIDCConfig = await dbCore.getScopedFullConfig(db, {
|
const config = await configs.getOIDCConfig()
|
||||||
type: ConfigType.OIDC,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!oidcConfig) {
|
if (!config) {
|
||||||
ctx.body = {}
|
ctx.body = []
|
||||||
} else {
|
} else {
|
||||||
ctx.body = oidcConfig.config.configs.map(config => ({
|
ctx.body = [
|
||||||
logo: config.logo,
|
{
|
||||||
name: config.name,
|
logo: config.logo,
|
||||||
uuid: config.uuid,
|
name: config.name,
|
||||||
}))
|
uuid: config.uuid,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.throw(err.status, err)
|
ctx.throw(err.status, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function publicSettings(ctx: Ctx) {
|
export async function publicSettings(
|
||||||
const db = tenancy.getGlobalDB()
|
ctx: Ctx<void, GetPublicSettingsResponse>
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
// Find the config with the most granular scope based on context
|
// settings
|
||||||
const publicConfig = await dbCore.getScopedFullConfig(db, {
|
const configDoc = await configs.getSettingsConfigDoc()
|
||||||
type: ConfigType.SETTINGS,
|
const config = configDoc.config
|
||||||
})
|
// enrich the logo url - empty url means deleted
|
||||||
|
if (config.logoUrl && config.logoUrl !== "") {
|
||||||
const googleConfig = await dbCore.getScopedFullConfig(db, {
|
config.logoUrl = objectStore.getGlobalFileUrl(
|
||||||
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 !== "") {
|
|
||||||
config.config.logoUrl = objectStore.getGlobalFileUrl(
|
|
||||||
"settings",
|
"settings",
|
||||||
"logoUrl",
|
"logoUrl",
|
||||||
config.config.logoUrlEtag
|
config.logoUrlEtag
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// google button flag
|
// google
|
||||||
if (googleConfig && googleConfig.config) {
|
const googleConfig = await configs.getGoogleConfig()
|
||||||
// activated by default for configs pre-activated flag
|
const preActivated = googleConfig?.activated == null
|
||||||
config.config.google =
|
const google = preActivated || !!googleConfig?.activated
|
||||||
googleConfig.config.activated == null || googleConfig.config.activated
|
const _googleCallbackUrl = await googleCallbackUrl(googleConfig)
|
||||||
} else {
|
|
||||||
config.config.google = false
|
// oidc
|
||||||
|
const oidcConfig = await configs.getOIDCConfig()
|
||||||
|
const oidc = oidcConfig?.activated || false
|
||||||
|
const _oidcCallbackUrl = await oidcCallbackUrl()
|
||||||
|
|
||||||
|
// sso enforced
|
||||||
|
const isSSOEnforced = await pro.features.isSSOEnforced({ config })
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
type: ConfigType.SETTINGS,
|
||||||
|
_id: configDoc._id,
|
||||||
|
_rev: configDoc._rev,
|
||||||
|
config: {
|
||||||
|
...config,
|
||||||
|
google,
|
||||||
|
oidc,
|
||||||
|
isSSOEnforced,
|
||||||
|
oidcCallbackUrl: _oidcCallbackUrl,
|
||||||
|
googleCallbackUrl: _googleCallbackUrl,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
} catch (err: any) {
|
||||||
ctx.throw(err.status, err)
|
ctx.throw(err.status, err)
|
||||||
}
|
}
|
||||||
|
@ -319,12 +266,11 @@ export async function upload(ctx: UserCtx) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// add to configuration structure
|
// add to configuration structure
|
||||||
// TODO: right now this only does a global level
|
let config = await configs.getConfig(type)
|
||||||
const db = tenancy.getGlobalDB()
|
if (!config) {
|
||||||
let cfgStructure = await dbCore.getScopedFullConfig(db, { type })
|
config = {
|
||||||
if (!cfgStructure) {
|
_id: configs.generateConfigID(type),
|
||||||
cfgStructure = {
|
type,
|
||||||
_id: dbCore.generateConfigID({ type }),
|
|
||||||
config: {},
|
config: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,14 +278,14 @@ export async function upload(ctx: UserCtx) {
|
||||||
// save the Etag for cache bursting
|
// save the Etag for cache bursting
|
||||||
const etag = result.ETag
|
const etag = result.ETag
|
||||||
if (etag) {
|
if (etag) {
|
||||||
cfgStructure.config[`${name}Etag`] = etag.replace(/"/g, "")
|
config.config[`${name}Etag`] = etag.replace(/"/g, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// save the file key
|
// save the file key
|
||||||
cfgStructure.config[`${name}`] = key
|
config.config[`${name}`] = key
|
||||||
|
|
||||||
// write back to db
|
// write back to db
|
||||||
await db.put(cfgStructure)
|
await configs.save(config)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "File has been uploaded and url stored to config.",
|
message: "File has been uploaded and url stored to config.",
|
||||||
|
@ -360,7 +306,6 @@ export async function destroy(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function configChecklist(ctx: Ctx) {
|
export async function configChecklist(ctx: Ctx) {
|
||||||
const db = tenancy.getGlobalDB()
|
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = tenancy.getTenantId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -375,19 +320,13 @@ export async function configChecklist(ctx: Ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// They have set up SMTP
|
// They have set up SMTP
|
||||||
const smtpConfig = await dbCore.getScopedFullConfig(db, {
|
const smtpConfig = await configs.getSMTPConfig()
|
||||||
type: ConfigType.SMTP,
|
|
||||||
})
|
|
||||||
|
|
||||||
// They have set up Google Auth
|
// They have set up Google Auth
|
||||||
const googleConfig = await dbCore.getScopedFullConfig(db, {
|
const googleConfig = await configs.getGoogleConfig()
|
||||||
type: ConfigType.GOOGLE,
|
|
||||||
})
|
|
||||||
|
|
||||||
// They have set up OIDC
|
// They have set up OIDC
|
||||||
const oidcConfig = await dbCore.getScopedFullConfig(db, {
|
const oidcConfig = await configs.getOIDCConfig()
|
||||||
type: ConfigType.OIDC,
|
|
||||||
})
|
|
||||||
|
|
||||||
// They have set up a global user
|
// They have set up a global user
|
||||||
const userExists = await checkAnyUserExists()
|
const userExists = await checkAnyUserExists()
|
||||||
|
|
|
@ -104,13 +104,7 @@ router
|
||||||
controller.save
|
controller.save
|
||||||
)
|
)
|
||||||
.delete("/api/global/configs/:id/:rev", auth.adminOnly, controller.destroy)
|
.delete("/api/global/configs/:id/:rev", auth.adminOnly, controller.destroy)
|
||||||
.get("/api/global/configs", controller.fetch)
|
|
||||||
.get("/api/global/configs/checklist", controller.configChecklist)
|
.get("/api/global/configs/checklist", controller.configChecklist)
|
||||||
.get(
|
|
||||||
"/api/global/configs/all/:type",
|
|
||||||
buildConfigGetValidation(),
|
|
||||||
controller.fetch
|
|
||||||
)
|
|
||||||
.get("/api/global/configs/public", controller.publicSettings)
|
.get("/api/global/configs/public", controller.publicSettings)
|
||||||
.get("/api/global/configs/public/oidc", controller.publicOidc)
|
.get("/api/global/configs/public/oidc", controller.publicOidc)
|
||||||
.get("/api/global/configs/:type", buildConfigGetValidation(), controller.find)
|
.get("/api/global/configs/:type", buildConfigGetValidation(), controller.find)
|
||||||
|
|
|
@ -110,7 +110,7 @@ describe("/api/global/auth", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
message: "SSO user cannot login using password",
|
message: "Password login is disabled for this user",
|
||||||
status: 400,
|
status: 400,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -175,7 +175,7 @@ describe("/api/global/auth", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(res.body).toEqual({
|
expect(res.body).toEqual({
|
||||||
message: "SSO user cannot reset password",
|
message: "Password reset is disabled for this user",
|
||||||
status: 400,
|
status: 400,
|
||||||
error: {
|
error: {
|
||||||
code: "http",
|
code: "http",
|
||||||
|
@ -367,7 +367,7 @@ describe("/api/global/auth", () => {
|
||||||
|
|
||||||
const res = await config.api.configs.OIDCCallback(configId, preAuthRes)
|
const res = await config.api.configs.OIDCCallback(configId, preAuthRes)
|
||||||
|
|
||||||
expect(events.auth.login).toBeCalledWith("oidc")
|
expect(events.auth.login).toBeCalledWith("oidc", "oauth@example.com")
|
||||||
expect(events.auth.login).toBeCalledTimes(1)
|
expect(events.auth.login).toBeCalledTimes(1)
|
||||||
expect(res.status).toBe(302)
|
expect(res.status).toBe(302)
|
||||||
const location: string = res.get("location")
|
const location: string = res.get("location")
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
import { TestConfiguration, structures, mocks } from "../../../../tests"
|
import { TestConfiguration, structures, mocks } from "../../../../tests"
|
||||||
mocks.email.mock()
|
mocks.email.mock()
|
||||||
import { Config, events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
|
import { GetPublicSettingsResponse, Config, ConfigType } from "@budibase/types"
|
||||||
|
|
||||||
describe("configs", () => {
|
describe("configs", () => {
|
||||||
const config = new TestConfiguration()
|
const config = new TestConfiguration()
|
||||||
|
@ -19,22 +20,29 @@ describe("configs", () => {
|
||||||
await config.afterAll()
|
await config.afterAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("post /api/global/configs", () => {
|
const saveConfig = async (conf: Config, _id?: string, _rev?: string) => {
|
||||||
const saveConfig = async (conf: any, _id?: string, _rev?: string) => {
|
const data = {
|
||||||
const data = {
|
...conf,
|
||||||
...conf,
|
_id,
|
||||||
_id,
|
_rev,
|
||||||
_rev,
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await config.api.configs.saveConfig(data)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
...res.body,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const res = await config.api.configs.saveConfig(data)
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
...res.body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSettingsConfig = async (
|
||||||
|
conf?: any,
|
||||||
|
_id?: string,
|
||||||
|
_rev?: string
|
||||||
|
) => {
|
||||||
|
const settingsConfig = structures.configs.settings(conf)
|
||||||
|
return saveConfig(settingsConfig, _id, _rev)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("POST /api/global/configs", () => {
|
||||||
describe("google", () => {
|
describe("google", () => {
|
||||||
const saveGoogleConfig = async (
|
const saveGoogleConfig = async (
|
||||||
conf?: any,
|
conf?: any,
|
||||||
|
@ -49,20 +57,20 @@ describe("configs", () => {
|
||||||
it("should create activated google config", async () => {
|
it("should create activated google config", async () => {
|
||||||
await saveGoogleConfig()
|
await saveGoogleConfig()
|
||||||
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOCreated).toBeCalledWith(Config.GOOGLE)
|
expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE)
|
||||||
expect(events.auth.SSODeactivated).not.toBeCalled()
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
||||||
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOActivated).toBeCalledWith(Config.GOOGLE)
|
expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE)
|
||||||
await config.deleteConfig(Config.GOOGLE)
|
await config.deleteConfig(ConfigType.GOOGLE)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create deactivated google config", async () => {
|
it("should create deactivated google config", async () => {
|
||||||
await saveGoogleConfig({ activated: false })
|
await saveGoogleConfig({ activated: false })
|
||||||
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOCreated).toBeCalledWith(Config.GOOGLE)
|
expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE)
|
||||||
expect(events.auth.SSOActivated).not.toBeCalled()
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
||||||
expect(events.auth.SSODeactivated).not.toBeCalled()
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
||||||
await config.deleteConfig(Config.GOOGLE)
|
await config.deleteConfig(ConfigType.GOOGLE)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -76,11 +84,11 @@ describe("configs", () => {
|
||||||
googleConf._rev
|
googleConf._rev
|
||||||
)
|
)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledWith(Config.GOOGLE)
|
expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE)
|
||||||
expect(events.auth.SSOActivated).not.toBeCalled()
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
||||||
expect(events.auth.SSODeactivated).toBeCalledTimes(1)
|
expect(events.auth.SSODeactivated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSODeactivated).toBeCalledWith(Config.GOOGLE)
|
expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.GOOGLE)
|
||||||
await config.deleteConfig(Config.GOOGLE)
|
await config.deleteConfig(ConfigType.GOOGLE)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update google config to activated", async () => {
|
it("should update google config to activated", async () => {
|
||||||
|
@ -92,11 +100,11 @@ describe("configs", () => {
|
||||||
googleConf._rev
|
googleConf._rev
|
||||||
)
|
)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledWith(Config.GOOGLE)
|
expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE)
|
||||||
expect(events.auth.SSODeactivated).not.toBeCalled()
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
||||||
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOActivated).toBeCalledWith(Config.GOOGLE)
|
expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE)
|
||||||
await config.deleteConfig(Config.GOOGLE)
|
await config.deleteConfig(ConfigType.GOOGLE)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -115,20 +123,20 @@ describe("configs", () => {
|
||||||
it("should create activated OIDC config", async () => {
|
it("should create activated OIDC config", async () => {
|
||||||
await saveOIDCConfig()
|
await saveOIDCConfig()
|
||||||
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC)
|
expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC)
|
||||||
expect(events.auth.SSODeactivated).not.toBeCalled()
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
||||||
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC)
|
expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC)
|
||||||
await config.deleteConfig(Config.OIDC)
|
await config.deleteConfig(ConfigType.OIDC)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create deactivated OIDC config", async () => {
|
it("should create deactivated OIDC config", async () => {
|
||||||
await saveOIDCConfig({ activated: false })
|
await saveOIDCConfig({ activated: false })
|
||||||
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC)
|
expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC)
|
||||||
expect(events.auth.SSOActivated).not.toBeCalled()
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
||||||
expect(events.auth.SSODeactivated).not.toBeCalled()
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
||||||
await config.deleteConfig(Config.OIDC)
|
await config.deleteConfig(ConfigType.OIDC)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -142,11 +150,11 @@ describe("configs", () => {
|
||||||
oidcConf._rev
|
oidcConf._rev
|
||||||
)
|
)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC)
|
expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.OIDC)
|
||||||
expect(events.auth.SSOActivated).not.toBeCalled()
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
||||||
expect(events.auth.SSODeactivated).toBeCalledTimes(1)
|
expect(events.auth.SSODeactivated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSODeactivated).toBeCalledWith(Config.OIDC)
|
expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.OIDC)
|
||||||
await config.deleteConfig(Config.OIDC)
|
await config.deleteConfig(ConfigType.OIDC)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update OIDC config to activated", async () => {
|
it("should update OIDC config to activated", async () => {
|
||||||
|
@ -158,11 +166,11 @@ describe("configs", () => {
|
||||||
oidcConf._rev
|
oidcConf._rev
|
||||||
)
|
)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC)
|
expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.OIDC)
|
||||||
expect(events.auth.SSODeactivated).not.toBeCalled()
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
||||||
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC)
|
expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC)
|
||||||
await config.deleteConfig(Config.OIDC)
|
await config.deleteConfig(ConfigType.OIDC)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -179,11 +187,11 @@ describe("configs", () => {
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("should create SMTP config", async () => {
|
it("should create SMTP config", async () => {
|
||||||
await config.deleteConfig(Config.SMTP)
|
await config.deleteConfig(ConfigType.SMTP)
|
||||||
await saveSMTPConfig()
|
await saveSMTPConfig()
|
||||||
expect(events.email.SMTPUpdated).not.toBeCalled()
|
expect(events.email.SMTPUpdated).not.toBeCalled()
|
||||||
expect(events.email.SMTPCreated).toBeCalledTimes(1)
|
expect(events.email.SMTPCreated).toBeCalledTimes(1)
|
||||||
await config.deleteConfig(Config.SMTP)
|
await config.deleteConfig(ConfigType.SMTP)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -194,24 +202,15 @@ describe("configs", () => {
|
||||||
await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev)
|
await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev)
|
||||||
expect(events.email.SMTPCreated).not.toBeCalled()
|
expect(events.email.SMTPCreated).not.toBeCalled()
|
||||||
expect(events.email.SMTPUpdated).toBeCalledTimes(1)
|
expect(events.email.SMTPUpdated).toBeCalledTimes(1)
|
||||||
await config.deleteConfig(Config.SMTP)
|
await config.deleteConfig(ConfigType.SMTP)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("settings", () => {
|
describe("settings", () => {
|
||||||
const saveSettingsConfig = async (
|
|
||||||
conf?: any,
|
|
||||||
_id?: string,
|
|
||||||
_rev?: string
|
|
||||||
) => {
|
|
||||||
const settingsConfig = structures.configs.settings(conf)
|
|
||||||
return saveConfig(settingsConfig, _id, _rev)
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("should create settings config with default settings", async () => {
|
it("should create settings config with default settings", async () => {
|
||||||
await config.deleteConfig(Config.SETTINGS)
|
await config.deleteConfig(ConfigType.SETTINGS)
|
||||||
|
|
||||||
await saveSettingsConfig()
|
await saveSettingsConfig()
|
||||||
|
|
||||||
|
@ -222,7 +221,7 @@ describe("configs", () => {
|
||||||
|
|
||||||
it("should create settings config with non-default settings", async () => {
|
it("should create settings config with non-default settings", async () => {
|
||||||
config.selfHosted()
|
config.selfHosted()
|
||||||
await config.deleteConfig(Config.SETTINGS)
|
await config.deleteConfig(ConfigType.SETTINGS)
|
||||||
const conf = {
|
const conf = {
|
||||||
company: "acme",
|
company: "acme",
|
||||||
logoUrl: "http://example.com",
|
logoUrl: "http://example.com",
|
||||||
|
@ -241,7 +240,7 @@ describe("configs", () => {
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("should update settings config", async () => {
|
it("should update settings config", async () => {
|
||||||
config.selfHosted()
|
config.selfHosted()
|
||||||
await config.deleteConfig(Config.SETTINGS)
|
await config.deleteConfig(ConfigType.SETTINGS)
|
||||||
const settingsConfig = await saveSettingsConfig()
|
const settingsConfig = await saveSettingsConfig()
|
||||||
settingsConfig.config.company = "acme"
|
settingsConfig.config.company = "acme"
|
||||||
settingsConfig.config.logoUrl = "http://example.com"
|
settingsConfig.config.logoUrl = "http://example.com"
|
||||||
|
@ -262,14 +261,43 @@ describe("configs", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return the correct checklist status based on the state of the budibase installation", async () => {
|
describe("GET /api/global/configs/checklist", () => {
|
||||||
await config.saveSmtpConfig()
|
it("should return the correct checklist", async () => {
|
||||||
|
await config.saveSmtpConfig()
|
||||||
|
|
||||||
const res = await config.api.configs.getConfigChecklist()
|
const res = await config.api.configs.getConfigChecklist()
|
||||||
const checklist = res.body
|
const checklist = res.body
|
||||||
|
|
||||||
expect(checklist.apps.checked).toBeFalsy()
|
expect(checklist.apps.checked).toBeFalsy()
|
||||||
expect(checklist.smtp.checked).toBeTruthy()
|
expect(checklist.smtp.checked).toBeTruthy()
|
||||||
expect(checklist.adminUser.checked).toBeTruthy()
|
expect(checklist.adminUser.checked).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /api/global/configs/public", () => {
|
||||||
|
it("should return the expected public settings", async () => {
|
||||||
|
await saveSettingsConfig()
|
||||||
|
|
||||||
|
const res = await config.api.configs.getPublicSettings()
|
||||||
|
const body = res.body as GetPublicSettingsResponse
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
_id: "config_settings",
|
||||||
|
type: "settings",
|
||||||
|
config: {
|
||||||
|
company: "Budibase",
|
||||||
|
logoUrl: "",
|
||||||
|
analyticsEnabled: false,
|
||||||
|
google: true,
|
||||||
|
googleCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/google/callback`,
|
||||||
|
isSSOEnforced: false,
|
||||||
|
oidc: false,
|
||||||
|
oidcCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/oidc/callback`,
|
||||||
|
platformUrl: "http://localhost:10000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
delete body._rev
|
||||||
|
expect(body).toEqual(expected)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
jest.unmock("node-fetch")
|
||||||
import { TestConfiguration } from "../../../../tests"
|
import { TestConfiguration } from "../../../../tests"
|
||||||
import { EmailTemplatePurpose } from "../../../../constants"
|
import { EmailTemplatePurpose } from "../../../../constants"
|
||||||
const nodemailer = require("nodemailer")
|
const nodemailer = require("nodemailer")
|
||||||
|
|
|
@ -26,8 +26,6 @@ function parseIntSafe(number: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
|
||||||
|
|
||||||
const environment = {
|
const environment = {
|
||||||
// auth
|
// auth
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
|
@ -51,7 +49,7 @@ const environment = {
|
||||||
CLUSTER_PORT: process.env.CLUSTER_PORT,
|
CLUSTER_PORT: process.env.CLUSTER_PORT,
|
||||||
// flags
|
// flags
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
SELF_HOSTED: selfHosted,
|
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
||||||
LOG_LEVEL: process.env.LOG_LEVEL,
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||||
|
@ -71,14 +69,6 @@ const environment = {
|
||||||
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
||||||
*/
|
*/
|
||||||
ENABLE_EMAIL_TEST_MODE: process.env.ENABLE_EMAIL_TEST_MODE,
|
ENABLE_EMAIL_TEST_MODE: process.env.ENABLE_EMAIL_TEST_MODE,
|
||||||
/**
|
|
||||||
* Enable to allow an admin user to login using a password.
|
|
||||||
* This can be useful to prevent lockout when configuring SSO.
|
|
||||||
* However, this should be turned OFF by default for security purposes.
|
|
||||||
*/
|
|
||||||
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
|
||||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
|
||||||
: false,
|
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -13,8 +13,15 @@ import { Event } from "@sentry/types/dist/event"
|
||||||
import Application from "koa"
|
import Application from "koa"
|
||||||
import { bootstrap } from "global-agent"
|
import { bootstrap } from "global-agent"
|
||||||
import * as db from "./db"
|
import * as db from "./db"
|
||||||
import { auth, logging, events, middleware } from "@budibase/backend-core"
|
import { sdk as proSdk } from "@budibase/pro"
|
||||||
import { sdk as proSdk, sdk } from "@budibase/pro"
|
import {
|
||||||
|
auth,
|
||||||
|
logging,
|
||||||
|
events,
|
||||||
|
middleware,
|
||||||
|
queue,
|
||||||
|
env as coreEnv,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
db.init()
|
db.init()
|
||||||
import Koa from "koa"
|
import Koa from "koa"
|
||||||
import koaBody from "koa-body"
|
import koaBody from "koa-body"
|
||||||
|
@ -32,7 +39,7 @@ import destroyable from "server-destroy"
|
||||||
// can't integrate directly into backend-core due to cyclic issues
|
// can't integrate directly into backend-core due to cyclic issues
|
||||||
events.processors.init(proSdk.auditLogs.write)
|
events.processors.init(proSdk.auditLogs.write)
|
||||||
|
|
||||||
if (env.ENABLE_SSO_MAINTENANCE_MODE) {
|
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
|
"Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
|
||||||
)
|
)
|
||||||
|
@ -86,6 +93,7 @@ server.on("close", async () => {
|
||||||
console.log("Server Closed")
|
console.log("Server Closed")
|
||||||
await redis.shutdown()
|
await redis.shutdown()
|
||||||
await events.shutdown()
|
await events.shutdown()
|
||||||
|
await queue.shutdown()
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
process.exit(errCode)
|
process.exit(errCode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,8 @@ export const reset = async (email: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// exit if user has sso
|
// exit if user has sso
|
||||||
if (await userSdk.isPreventSSOPasswords(user)) {
|
if (await userSdk.isPreventPasswordActions(user)) {
|
||||||
throw new HTTPError("SSO user cannot reset password", 400)
|
throw new HTTPError("Password reset is disabled for this user", 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
// send password reset
|
// send password reset
|
||||||
|
|
|
@ -1,26 +1,50 @@
|
||||||
import { structures } from "../../../tests"
|
import { structures } from "../../../tests"
|
||||||
import * as users from "../users"
|
|
||||||
import env from "../../../environment"
|
|
||||||
import { mocks } from "@budibase/backend-core/tests"
|
import { mocks } from "@budibase/backend-core/tests"
|
||||||
|
import { env } from "@budibase/backend-core"
|
||||||
|
import * as users from "../users"
|
||||||
import { CloudAccount } from "@budibase/types"
|
import { CloudAccount } from "@budibase/types"
|
||||||
|
import { isPreventPasswordActions } from "../users"
|
||||||
|
|
||||||
|
jest.mock("@budibase/pro")
|
||||||
|
import * as _pro from "@budibase/pro"
|
||||||
|
const pro = jest.mocked(_pro, true)
|
||||||
|
|
||||||
describe("users", () => {
|
describe("users", () => {
|
||||||
describe("isPreventSSOPasswords", () => {
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isPreventPasswordActions", () => {
|
||||||
|
it("returns false for non sso user", async () => {
|
||||||
|
const user = structures.users.user()
|
||||||
|
const result = await users.isPreventPasswordActions(user)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it("returns true for sso account user", async () => {
|
it("returns true for sso account user", async () => {
|
||||||
const user = structures.users.user()
|
const user = structures.users.user()
|
||||||
mocks.accounts.getAccount.mockReturnValue(
|
mocks.accounts.getAccount.mockReturnValue(
|
||||||
Promise.resolve(structures.accounts.ssoAccount() as CloudAccount)
|
Promise.resolve(structures.accounts.ssoAccount() as CloudAccount)
|
||||||
)
|
)
|
||||||
const result = await users.isPreventSSOPasswords(user)
|
const result = await users.isPreventPasswordActions(user)
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns true for sso user", async () => {
|
it("returns true for sso user", async () => {
|
||||||
const user = structures.users.ssoUser()
|
const user = structures.users.ssoUser()
|
||||||
const result = await users.isPreventSSOPasswords(user)
|
const result = await users.isPreventPasswordActions(user)
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("enforced sso", () => {
|
||||||
|
it("returns true for all users when sso is enforced", async () => {
|
||||||
|
const user = structures.users.user()
|
||||||
|
pro.features.isSSOEnforced.mockReturnValue(Promise.resolve(true))
|
||||||
|
const result = await users.isPreventPasswordActions(user)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("sso maintenance mode", () => {
|
describe("sso maintenance mode", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
env._set("ENABLE_SSO_MAINTENANCE_MODE", true)
|
env._set("ENABLE_SSO_MAINTENANCE_MODE", true)
|
||||||
|
@ -33,7 +57,7 @@ describe("users", () => {
|
||||||
describe("non-admin user", () => {
|
describe("non-admin user", () => {
|
||||||
it("returns true", async () => {
|
it("returns true", async () => {
|
||||||
const user = structures.users.ssoUser()
|
const user = structures.users.ssoUser()
|
||||||
const result = await users.isPreventSSOPasswords(user)
|
const result = await users.isPreventPasswordActions(user)
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -43,7 +67,7 @@ describe("users", () => {
|
||||||
const user = structures.users.ssoUser({
|
const user = structures.users.ssoUser({
|
||||||
user: structures.users.adminUser(),
|
user: structures.users.adminUser(),
|
||||||
})
|
})
|
||||||
const result = await users.isPreventSSOPasswords(user)
|
const result = await users.isPreventPasswordActions(user)
|
||||||
expect(result).toBe(false)
|
expect(result).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
users as usersCore,
|
users as usersCore,
|
||||||
utils,
|
utils,
|
||||||
ViewName,
|
ViewName,
|
||||||
|
env as coreEnv,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
AccountMetadata,
|
AccountMetadata,
|
||||||
|
@ -34,7 +35,7 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { sendEmail } from "../../utilities/email"
|
import { sendEmail } from "../../utilities/email"
|
||||||
import { EmailTemplatePurpose } from "../../constants"
|
import { EmailTemplatePurpose } from "../../constants"
|
||||||
import { groups as groupsSdk } from "@budibase/pro"
|
import * as pro from "@budibase/pro"
|
||||||
import * as accountSdk from "../accounts"
|
import * as accountSdk from "../accounts"
|
||||||
|
|
||||||
const PAGE_LIMIT = 8
|
const PAGE_LIMIT = 8
|
||||||
|
@ -122,8 +123,8 @@ const buildUser = async (
|
||||||
|
|
||||||
let hashedPassword
|
let hashedPassword
|
||||||
if (password) {
|
if (password) {
|
||||||
if (await isPreventSSOPasswords(user)) {
|
if (await isPreventPasswordActions(user)) {
|
||||||
throw new HTTPError("SSO user cannot set password", 400)
|
throw new HTTPError("Password change is disabled for this user", 400)
|
||||||
}
|
}
|
||||||
hashedPassword = opts.hashPassword ? await utils.hash(password) : password
|
hashedPassword = opts.hashPassword ? await utils.hash(password) : password
|
||||||
} else if (dbUser) {
|
} else if (dbUser) {
|
||||||
|
@ -188,13 +189,18 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isPreventSSOPasswords(user: User) {
|
export async function isPreventPasswordActions(user: User) {
|
||||||
// when in maintenance mode we allow sso users with the admin role
|
// when in maintenance mode we allow sso users with the admin role
|
||||||
// to perform any password action - this prevents lockout
|
// to perform any password action - this prevents lockout
|
||||||
if (env.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
|
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSO is enforced for all users
|
||||||
|
if (await pro.features.isSSOEnforced()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Check local sso
|
// Check local sso
|
||||||
if (isSSOUser(user)) {
|
if (isSSOUser(user)) {
|
||||||
return true
|
return true
|
||||||
|
@ -278,7 +284,7 @@ export const save = async (
|
||||||
|
|
||||||
if (userGroups.length > 0) {
|
if (userGroups.length > 0) {
|
||||||
for (let groupId of userGroups) {
|
for (let groupId of userGroups) {
|
||||||
groupPromises.push(groupsSdk.addUsers(groupId, [_id]))
|
groupPromises.push(pro.groups.addUsers(groupId, [_id]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -456,7 +462,7 @@ export const bulkCreate = async (
|
||||||
const groupPromises = []
|
const groupPromises = []
|
||||||
const createdUserIds = saved.map(user => user._id)
|
const createdUserIds = saved.map(user => user._id)
|
||||||
for (let groupId of groups) {
|
for (let groupId of groups) {
|
||||||
groupPromises.push(groupsSdk.addUsers(groupId, createdUserIds))
|
groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
|
||||||
}
|
}
|
||||||
await Promise.all(groupPromises)
|
await Promise.all(groupPromises)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,14 @@ export class ConfigAPI extends TestAPI {
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPublicSettings = () => {
|
||||||
|
return this.request
|
||||||
|
.get(`/api/global/configs/public`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect(200)
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
}
|
||||||
|
|
||||||
saveConfig = (data: any) => {
|
saveConfig = (data: any) => {
|
||||||
return this.request
|
return this.request
|
||||||
.post(`/api/global/configs`)
|
.post(`/api/global/configs`)
|
||||||
|
|
|
@ -13,6 +13,7 @@ export class EmailAPI extends TestAPI {
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
purpose,
|
purpose,
|
||||||
tenantId: this.config.getTenantId(),
|
tenantId: this.config.getTenantId(),
|
||||||
|
userId: this.config.user?._id!,
|
||||||
})
|
})
|
||||||
.set(this.config.defaultHeaders())
|
.set(this.config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { Config } from "../../constants"
|
|
||||||
import { utils } from "@budibase/backend-core"
|
import { utils } from "@budibase/backend-core"
|
||||||
|
import {
|
||||||
|
SettingsConfig,
|
||||||
|
ConfigType,
|
||||||
|
SMTPConfig,
|
||||||
|
GoogleConfig,
|
||||||
|
OIDCConfig,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export function oidc(conf?: any) {
|
export function oidc(conf?: any): OIDCConfig {
|
||||||
return {
|
return {
|
||||||
type: Config.OIDC,
|
type: ConfigType.OIDC,
|
||||||
config: {
|
config: {
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
|
@ -21,9 +27,9 @@ export function oidc(conf?: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function google(conf?: any) {
|
export function google(conf?: any): GoogleConfig {
|
||||||
return {
|
return {
|
||||||
type: Config.GOOGLE,
|
type: ConfigType.GOOGLE,
|
||||||
config: {
|
config: {
|
||||||
clientID: "clientId",
|
clientID: "clientId",
|
||||||
clientSecret: "clientSecret",
|
clientSecret: "clientSecret",
|
||||||
|
@ -33,9 +39,9 @@ export function google(conf?: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function smtp(conf?: any) {
|
export function smtp(conf?: any): SMTPConfig {
|
||||||
return {
|
return {
|
||||||
type: Config.SMTP,
|
type: ConfigType.SMTP,
|
||||||
config: {
|
config: {
|
||||||
port: 12345,
|
port: 12345,
|
||||||
host: "smtptesthost.com",
|
host: "smtptesthost.com",
|
||||||
|
@ -47,25 +53,26 @@ export function smtp(conf?: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function smtpEthereal() {
|
export function smtpEthereal(): SMTPConfig {
|
||||||
return {
|
return {
|
||||||
type: Config.SMTP,
|
type: ConfigType.SMTP,
|
||||||
config: {
|
config: {
|
||||||
port: 587,
|
port: 587,
|
||||||
host: "smtp.ethereal.email",
|
host: "smtp.ethereal.email",
|
||||||
|
from: "testfrom@test.com",
|
||||||
secure: false,
|
secure: false,
|
||||||
auth: {
|
auth: {
|
||||||
user: "don.bahringer@ethereal.email",
|
user: "wyatt.zulauf29@ethereal.email",
|
||||||
pass: "yCKSH8rWyUPbnhGYk9",
|
pass: "tEwDtHBWWxusVWAPfa",
|
||||||
},
|
},
|
||||||
connectionTimeout: 1000, // must be less than the jest default of 5000
|
connectionTimeout: 1000, // must be less than the jest default of 5000
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function settings(conf?: any) {
|
export function settings(conf?: any): SettingsConfig {
|
||||||
return {
|
return {
|
||||||
type: Config.SETTINGS,
|
type: ConfigType.SETTINGS,
|
||||||
config: {
|
config: {
|
||||||
platformUrl: "http://localhost:10000",
|
platformUrl: "http://localhost:10000",
|
||||||
logoUrl: "",
|
logoUrl: "",
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { EmailTemplatePurpose, TemplateType, Config } from "../constants"
|
import { EmailTemplatePurpose, TemplateType } from "../constants"
|
||||||
import { getTemplateByPurpose } from "../constants/templates"
|
import { getTemplateByPurpose } from "../constants/templates"
|
||||||
import { getSettingsTemplateContext } from "./templates"
|
import { getSettingsTemplateContext } from "./templates"
|
||||||
import { processString } from "@budibase/string-templates"
|
import { processString } from "@budibase/string-templates"
|
||||||
import { getResetPasswordCode, getInviteCode } from "./redis"
|
import { getResetPasswordCode, getInviteCode } from "./redis"
|
||||||
import { User, Database } from "@budibase/types"
|
import { User, SMTPInnerConfig } from "@budibase/types"
|
||||||
import { tenancy, db as dbCore } from "@budibase/backend-core"
|
import { configs } from "@budibase/backend-core"
|
||||||
const nodemailer = require("nodemailer")
|
const nodemailer = require("nodemailer")
|
||||||
|
|
||||||
type SendEmailOpts = {
|
type SendEmailOpts = {
|
||||||
|
@ -36,24 +36,24 @@ const FULL_EMAIL_PURPOSES = [
|
||||||
EmailTemplatePurpose.CUSTOM,
|
EmailTemplatePurpose.CUSTOM,
|
||||||
]
|
]
|
||||||
|
|
||||||
function createSMTPTransport(config: any) {
|
function createSMTPTransport(config?: SMTPInnerConfig) {
|
||||||
let options: any
|
let options: any
|
||||||
let secure = config.secure
|
let secure = config?.secure
|
||||||
// default it if not specified
|
// default it if not specified
|
||||||
if (secure == null) {
|
if (secure == null) {
|
||||||
secure = config.port === 465
|
secure = config?.port === 465
|
||||||
}
|
}
|
||||||
if (!TEST_MODE) {
|
if (!TEST_MODE) {
|
||||||
options = {
|
options = {
|
||||||
port: config.port,
|
port: config?.port,
|
||||||
host: config.host,
|
host: config?.host,
|
||||||
secure: secure,
|
secure: secure,
|
||||||
auth: config.auth,
|
auth: config?.auth,
|
||||||
}
|
}
|
||||||
options.tls = {
|
options.tls = {
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
}
|
}
|
||||||
if (config.connectionTimeout) {
|
if (config?.connectionTimeout) {
|
||||||
options.connectionTimeout = config.connectionTimeout
|
options.connectionTimeout = config.connectionTimeout
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -134,57 +134,16 @@ async function buildEmail(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function for finding most valid SMTP configuration.
|
|
||||||
* @param {object} db The CouchDB database which is to be looked up within.
|
|
||||||
* @param {string|null} workspaceId If using finer grain control of configs a workspace can be used.
|
|
||||||
* @param {boolean|null} automation Whether or not the configuration is being fetched for an email automation.
|
|
||||||
* @return {Promise<object|null>} returns the SMTP configuration if it exists
|
|
||||||
*/
|
|
||||||
async function getSmtpConfiguration(
|
|
||||||
db: Database,
|
|
||||||
workspaceId?: string,
|
|
||||||
automation?: boolean
|
|
||||||
) {
|
|
||||||
const params: any = {
|
|
||||||
type: Config.SMTP,
|
|
||||||
}
|
|
||||||
if (workspaceId) {
|
|
||||||
params.workspace = workspaceId
|
|
||||||
}
|
|
||||||
|
|
||||||
const customConfig = await dbCore.getScopedConfig(db, params)
|
|
||||||
|
|
||||||
if (customConfig) {
|
|
||||||
return customConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use an SMTP fallback configuration from env variables
|
|
||||||
if (!automation && env.SMTP_FALLBACK_ENABLED) {
|
|
||||||
return {
|
|
||||||
port: env.SMTP_PORT,
|
|
||||||
host: env.SMTP_HOST,
|
|
||||||
secure: false,
|
|
||||||
from: env.SMTP_FROM_ADDRESS,
|
|
||||||
auth: {
|
|
||||||
user: env.SMTP_USER,
|
|
||||||
pass: env.SMTP_PASSWORD,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a SMTP config exists based on passed in parameters.
|
* Checks if a SMTP config exists based on passed in parameters.
|
||||||
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
|
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
|
||||||
*/
|
*/
|
||||||
export async function isEmailConfigured(workspaceId?: string) {
|
export async function isEmailConfigured() {
|
||||||
// when "testing" or smtp fallback is enabled simply return true
|
// when "testing" or smtp fallback is enabled simply return true
|
||||||
if (TEST_MODE || env.SMTP_FALLBACK_ENABLED) {
|
if (TEST_MODE || env.SMTP_FALLBACK_ENABLED) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const db = tenancy.getGlobalDB()
|
const config = await configs.getSMTPConfig()
|
||||||
const config = await getSmtpConfiguration(db, workspaceId)
|
|
||||||
return config != null
|
return config != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,22 +161,17 @@ export async function sendEmail(
|
||||||
purpose: EmailTemplatePurpose,
|
purpose: EmailTemplatePurpose,
|
||||||
opts: SendEmailOpts
|
opts: SendEmailOpts
|
||||||
) {
|
) {
|
||||||
const db = tenancy.getGlobalDB()
|
const config = await configs.getSMTPConfig(opts?.automation)
|
||||||
let config =
|
if (!config && !TEST_MODE) {
|
||||||
(await getSmtpConfiguration(db, opts?.workspaceId, opts?.automation)) || {}
|
|
||||||
if (Object.keys(config).length === 0 && !TEST_MODE) {
|
|
||||||
throw "Unable to find SMTP configuration."
|
throw "Unable to find SMTP configuration."
|
||||||
}
|
}
|
||||||
const transport = createSMTPTransport(config)
|
const transport = createSMTPTransport(config)
|
||||||
// if there is a link code needed this will retrieve it
|
// if there is a link code needed this will retrieve it
|
||||||
const code = await getLinkCode(purpose, email, opts.user, opts?.info)
|
const code = await getLinkCode(purpose, email, opts.user, opts?.info)
|
||||||
let context
|
let context = await getSettingsTemplateContext(purpose, code)
|
||||||
if (code) {
|
|
||||||
context = await getSettingsTemplateContext(purpose, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
let message: any = {
|
let message: any = {
|
||||||
from: opts?.from || config.from,
|
from: opts?.from || config?.from,
|
||||||
html: await buildEmail(purpose, email, context, {
|
html: await buildEmail(purpose, email, context, {
|
||||||
user: opts?.user,
|
user: opts?.user,
|
||||||
contents: opts?.contents,
|
contents: opts?.contents,
|
||||||
|
@ -231,9 +185,9 @@ export async function sendEmail(
|
||||||
bcc: opts?.bcc,
|
bcc: opts?.bcc,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts?.subject || config.subject) {
|
if (opts?.subject || config?.subject) {
|
||||||
message.subject = await processString(
|
message.subject = await processString(
|
||||||
opts?.subject || config.subject,
|
(opts?.subject || config?.subject) as string,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { db as dbCore, tenancy } from "@budibase/backend-core"
|
import { tenancy, configs } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
Config,
|
|
||||||
InternalTemplateBinding,
|
InternalTemplateBinding,
|
||||||
LOGO_URL,
|
LOGO_URL,
|
||||||
EmailTemplatePurpose,
|
EmailTemplatePurpose,
|
||||||
|
@ -10,20 +9,16 @@ const BASE_COMPANY = "Budibase"
|
||||||
|
|
||||||
export async function getSettingsTemplateContext(
|
export async function getSettingsTemplateContext(
|
||||||
purpose: EmailTemplatePurpose,
|
purpose: EmailTemplatePurpose,
|
||||||
code?: string
|
code?: string | null
|
||||||
) {
|
) {
|
||||||
const db = tenancy.getGlobalDB()
|
let settings = await configs.getSettingsConfig()
|
||||||
// TODO: use more granular settings in the future if required
|
|
||||||
let settings =
|
|
||||||
(await dbCore.getScopedConfig(db, { type: Config.SETTINGS })) || {}
|
|
||||||
const URL = settings.platformUrl
|
const URL = settings.platformUrl
|
||||||
const context: any = {
|
const context: any = {
|
||||||
[InternalTemplateBinding.LOGO_URL]:
|
[InternalTemplateBinding.LOGO_URL]:
|
||||||
checkSlashesInUrl(`${URL}/${settings.logoUrl}`) || LOGO_URL,
|
checkSlashesInUrl(`${URL}/${settings.logoUrl}`) || LOGO_URL,
|
||||||
[InternalTemplateBinding.PLATFORM_URL]: URL,
|
[InternalTemplateBinding.PLATFORM_URL]: URL,
|
||||||
[InternalTemplateBinding.COMPANY]: settings.company || BASE_COMPANY,
|
[InternalTemplateBinding.COMPANY]: settings.company || BASE_COMPANY,
|
||||||
[InternalTemplateBinding.DOCS_URL]:
|
[InternalTemplateBinding.DOCS_URL]: "https://docs.budibase.com/",
|
||||||
settings.docsUrl || "https://docs.budibase.com/",
|
|
||||||
[InternalTemplateBinding.LOGIN_URL]: checkSlashesInUrl(
|
[InternalTemplateBinding.LOGIN_URL]: checkSlashesInUrl(
|
||||||
tenancy.addTenantToUrl(`${URL}/login`)
|
tenancy.addTenantToUrl(`${URL}/login`)
|
||||||
),
|
),
|
||||||
|
|
|
@ -475,14 +475,14 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/backend-core@2.3.18-alpha.12":
|
"@budibase/backend-core@2.3.18-alpha.13":
|
||||||
version "2.3.18-alpha.12"
|
version "2.3.18-alpha.13"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.12.tgz#ad1b16be64b78b596af2b5f75647c32e8f6f101a"
|
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.13.tgz#b797d7a4d30ff7f21e473334edb4086f5818830e"
|
||||||
integrity sha512-E1NEO+/sNkkRqn/xk9XQmFBO9/dl27w9EB0QGztti/16JV9NgxyDQCJIdGwlD08s1y/lUwOKk0TkSZJs+CTYDw==
|
integrity sha512-c6d0xCRgLlPeX1euAoQuoDOkMkDGQy/miBx/Z8xyU9bzDDTNqBTogxpxsNf8DdZG7EMJhsJlCUvb26Onz7/50A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/nano" "10.1.1"
|
"@budibase/nano" "10.1.1"
|
||||||
"@budibase/pouchdb-replication-stream" "1.2.10"
|
"@budibase/pouchdb-replication-stream" "1.2.10"
|
||||||
"@budibase/types" "2.3.18-alpha.12"
|
"@budibase/types" "2.3.18-alpha.13"
|
||||||
"@shopify/jest-koa-mocks" "5.0.1"
|
"@shopify/jest-koa-mocks" "5.0.1"
|
||||||
"@techpass/passport-openidconnect" "0.3.2"
|
"@techpass/passport-openidconnect" "0.3.2"
|
||||||
aws-cloudfront-sign "2.2.0"
|
aws-cloudfront-sign "2.2.0"
|
||||||
|
@ -539,13 +539,13 @@
|
||||||
pouchdb-promise "^6.0.4"
|
pouchdb-promise "^6.0.4"
|
||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
|
|
||||||
"@budibase/pro@2.3.18-alpha.12":
|
"@budibase/pro@2.3.18-alpha.13":
|
||||||
version "2.3.18-alpha.12"
|
version "2.3.18-alpha.13"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.12.tgz#be552b3a9f5850e746081540d6586aae69147bec"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.13.tgz#9f3dd0339184a58609c726f3f2b4580cf802dc78"
|
||||||
integrity sha512-M3b0njzSi47KH6uaQfYPoA2KWrjPiwcU3ONyaVWXHIktVrIKtYaFwOLBr/dmWGfMrL2297SSqg7V4DTaLyAhnw==
|
integrity sha512-pRroVVFGITFsFtzzH6LgzuaUBKldvFQxTgO7K6dYjjE7xwTCnZCg0E4L2Ew/JIY39UN2WEb5bwdt3+pqPH7Dmg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/backend-core" "2.3.18-alpha.12"
|
"@budibase/backend-core" "2.3.18-alpha.13"
|
||||||
"@budibase/types" "2.3.18-alpha.12"
|
"@budibase/types" "2.3.18-alpha.13"
|
||||||
"@koa/router" "8.0.8"
|
"@koa/router" "8.0.8"
|
||||||
bull "4.10.1"
|
bull "4.10.1"
|
||||||
joi "17.6.0"
|
joi "17.6.0"
|
||||||
|
@ -553,10 +553,10 @@
|
||||||
lru-cache "^7.14.1"
|
lru-cache "^7.14.1"
|
||||||
node-fetch "^2.6.1"
|
node-fetch "^2.6.1"
|
||||||
|
|
||||||
"@budibase/types@2.3.18-alpha.12":
|
"@budibase/types@2.3.18-alpha.13":
|
||||||
version "2.3.18-alpha.12"
|
version "2.3.18-alpha.13"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.12.tgz#a63eb978ccc7e55c209b3e9d71f9aecf7facc0d1"
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.13.tgz#e80cbf79249ed5cf8fe86e294d0b1e25e0a839ee"
|
||||||
integrity sha512-27o2BmI/HXIR3frZ8FtqHgAe1hd8jPIzgPaEhKrQiYJ/opUVccqupx9ld75Hyk9E6cdXu0UF0/+LxPpUmMugag==
|
integrity sha512-fmgpwMMGkbPOObmFnZZH8iKelycmhvBydGQuPEmIk1c449ysfiKF2Strnca6MaY1XtskfRz/nNdWGdGS1HhmFw==
|
||||||
|
|
||||||
"@cspotcode/source-map-support@^0.8.0":
|
"@cspotcode/source-map-support@^0.8.0":
|
||||||
version "0.8.1"
|
version "0.8.1"
|
||||||
|
|
Loading…
Reference in a new issue