Support path variable tenancy detection, add /api/system/* tests, update no tenancy matchers to be more accurate
This commit is contained in:
parent
ada0eb79bc
commit
0bad2dd9ae
|
@ -1,15 +1,16 @@
|
||||||
const { getGlobalUserParams, getAllApps } = require("../db/utils")
|
import { getGlobalUserParams, getAllApps } from "../db/utils"
|
||||||
const { doWithDB } = require("../db")
|
import { doWithDB } from "../db"
|
||||||
const { doWithGlobalDB } = require("../tenancy")
|
import { doWithGlobalDB } from "../tenancy"
|
||||||
const { StaticDatabases } = require("../db/constants")
|
import { StaticDatabases } from "../db/constants"
|
||||||
|
import { App, Tenants, User } from "@budibase/types"
|
||||||
|
|
||||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||||
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
|
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
|
||||||
|
|
||||||
const removeTenantFromInfoDB = async tenantId => {
|
const removeTenantFromInfoDB = async (tenantId: string) => {
|
||||||
try {
|
try {
|
||||||
await doWithDB(PLATFORM_INFO_DB, async infoDb => {
|
await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => {
|
||||||
let tenants = await infoDb.get(TENANT_DOC)
|
const tenants = (await infoDb.get(TENANT_DOC)) as Tenants
|
||||||
tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId)
|
tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId)
|
||||||
|
|
||||||
await infoDb.put(tenants)
|
await infoDb.put(tenants)
|
||||||
|
@ -20,14 +21,14 @@ const removeTenantFromInfoDB = async tenantId => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.removeUserFromInfoDB = async dbUser => {
|
export const removeUserFromInfoDB = async (dbUser: User) => {
|
||||||
await doWithDB(PLATFORM_INFO_DB, async infoDb => {
|
await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => {
|
||||||
const keys = [dbUser._id, dbUser.email]
|
const keys = [dbUser._id, dbUser.email]
|
||||||
const userDocs = await infoDb.allDocs({
|
const userDocs = await infoDb.allDocs({
|
||||||
keys,
|
keys,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
const toDelete = userDocs.rows.map(row => {
|
const toDelete = userDocs.rows.map((row: any) => {
|
||||||
return {
|
return {
|
||||||
...row.doc,
|
...row.doc,
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
|
@ -37,18 +38,18 @@ exports.removeUserFromInfoDB = async dbUser => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeUsersFromInfoDB = async tenantId => {
|
const removeUsersFromInfoDB = async (tenantId: string) => {
|
||||||
return doWithGlobalDB(tenantId, async db => {
|
return doWithGlobalDB(tenantId, async (db: any) => {
|
||||||
try {
|
try {
|
||||||
const allUsers = await db.allDocs(
|
const allUsers = await db.allDocs(
|
||||||
getGlobalUserParams(null, {
|
getGlobalUserParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
await doWithDB(PLATFORM_INFO_DB, async infoDb => {
|
await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => {
|
||||||
const allEmails = allUsers.rows.map(row => row.doc.email)
|
const allEmails = allUsers.rows.map((row: any) => row.doc.email)
|
||||||
// get the id docs
|
// get the id docs
|
||||||
let keys = allUsers.rows.map(row => row.id)
|
let keys = allUsers.rows.map((row: any) => row.id)
|
||||||
// and the email docs
|
// and the email docs
|
||||||
keys = keys.concat(allEmails)
|
keys = keys.concat(allEmails)
|
||||||
// retrieve the docs and delete them
|
// retrieve the docs and delete them
|
||||||
|
@ -56,7 +57,7 @@ const removeUsersFromInfoDB = async tenantId => {
|
||||||
keys,
|
keys,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
const toDelete = userDocs.rows.map(row => {
|
const toDelete = userDocs.rows.map((row: any) => {
|
||||||
return {
|
return {
|
||||||
...row.doc,
|
...row.doc,
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
|
@ -71,8 +72,8 @@ const removeUsersFromInfoDB = async tenantId => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeGlobalDB = async tenantId => {
|
const removeGlobalDB = async (tenantId: string) => {
|
||||||
return doWithGlobalDB(tenantId, async db => {
|
return doWithGlobalDB(tenantId, async (db: any) => {
|
||||||
try {
|
try {
|
||||||
await db.destroy()
|
await db.destroy()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -82,11 +83,11 @@ const removeGlobalDB = async tenantId => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeTenantApps = async tenantId => {
|
const removeTenantApps = async (tenantId: string) => {
|
||||||
try {
|
try {
|
||||||
const apps = await getAllApps({ all: true })
|
const apps = (await getAllApps({ all: true })) as App[]
|
||||||
const destroyPromises = apps.map(app =>
|
const destroyPromises = apps.map(app =>
|
||||||
doWithDB(app.appId, db => db.destroy())
|
doWithDB(app.appId, (db: any) => db.destroy())
|
||||||
)
|
)
|
||||||
await Promise.allSettled(destroyPromises)
|
await Promise.allSettled(destroyPromises)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -96,7 +97,7 @@ const removeTenantApps = async tenantId => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// can't live in tenancy package due to circular dependency on db/utils
|
// can't live in tenancy package due to circular dependency on db/utils
|
||||||
exports.deleteTenant = async tenantId => {
|
export const deleteTenant = async (tenantId: string) => {
|
||||||
await removeTenantFromInfoDB(tenantId)
|
await removeTenantFromInfoDB(tenantId)
|
||||||
await removeUsersFromInfoDB(tenantId)
|
await removeUsersFromInfoDB(tenantId)
|
||||||
await removeGlobalDB(tenantId)
|
await removeGlobalDB(tenantId)
|
|
@ -11,7 +11,7 @@ import env from "./environment"
|
||||||
import tenancy from "./tenancy"
|
import tenancy from "./tenancy"
|
||||||
import featureFlags from "./featureFlags"
|
import featureFlags from "./featureFlags"
|
||||||
import * as sessions from "./security/sessions"
|
import * as sessions from "./security/sessions"
|
||||||
import deprovisioning from "./context/deprovision"
|
import * as deprovisioning from "./context/deprovision"
|
||||||
import auth from "./auth"
|
import auth from "./auth"
|
||||||
import constants from "./constants"
|
import constants from "./constants"
|
||||||
import * as dbConstants from "./db/constants"
|
import * as dbConstants from "./db/constants"
|
||||||
|
|
|
@ -1,27 +1,35 @@
|
||||||
|
import { BBContext, EndpointMatcher, RegexMatcher } from "@budibase/types"
|
||||||
|
|
||||||
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g
|
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g
|
||||||
|
|
||||||
exports.buildMatcherRegex = patterns => {
|
export const buildMatcherRegex = (
|
||||||
|
patterns: EndpointMatcher[]
|
||||||
|
): RegexMatcher[] => {
|
||||||
if (!patterns) {
|
if (!patterns) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return patterns.map(pattern => {
|
return patterns.map(pattern => {
|
||||||
const isObj = typeof pattern === "object" && pattern.route
|
let route = pattern.route
|
||||||
const method = isObj ? pattern.method : "GET"
|
const method = pattern.method
|
||||||
const strict = pattern.strict ? pattern.strict : false
|
const strict = pattern.strict ? pattern.strict : false
|
||||||
let route = isObj ? pattern.route : pattern
|
|
||||||
|
|
||||||
|
// if there is a param in the route
|
||||||
|
// use a wildcard pattern
|
||||||
const matches = route.match(PARAM_REGEX)
|
const matches = route.match(PARAM_REGEX)
|
||||||
if (matches) {
|
if (matches) {
|
||||||
for (let match of matches) {
|
for (let match of matches) {
|
||||||
const pattern = "/.*" + (match.endsWith("/") ? "/" : "")
|
const suffix = match.endsWith("/") ? "/" : ""
|
||||||
|
const pattern = "/.*" + suffix
|
||||||
route = route.replace(match, pattern)
|
route = route.replace(match, pattern)
|
||||||
|
console.log(route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { regex: new RegExp(route), method, strict, route }
|
return { regex: new RegExp(route), method, strict, route }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.matches = (ctx, options) => {
|
export const matches = (ctx: BBContext, options: RegexMatcher[]) => {
|
||||||
return options.find(({ regex, method, strict, route }) => {
|
return options.find(({ regex, method, strict, route }) => {
|
||||||
let urlMatch
|
let urlMatch
|
||||||
if (strict) {
|
if (strict) {
|
|
@ -1,5 +1,6 @@
|
||||||
import { doInTenant, getTenantIDFromCtx } from "../tenancy"
|
import { doInTenant, getTenantIDFromCtx } from "../tenancy"
|
||||||
import { buildMatcherRegex, matches } from "./matchers"
|
import { buildMatcherRegex, matches } from "./matchers"
|
||||||
|
import { Headers } from "../constants"
|
||||||
import {
|
import {
|
||||||
BBContext,
|
BBContext,
|
||||||
EndpointMatcher,
|
EndpointMatcher,
|
||||||
|
@ -9,7 +10,7 @@ import {
|
||||||
|
|
||||||
const tenancy = (
|
const tenancy = (
|
||||||
allowQueryStringPatterns: EndpointMatcher[],
|
allowQueryStringPatterns: EndpointMatcher[],
|
||||||
noTenancyPatterns: EndpointMatcher,
|
noTenancyPatterns: EndpointMatcher[],
|
||||||
opts = { noTenancyRequired: false }
|
opts = { noTenancyRequired: false }
|
||||||
) => {
|
) => {
|
||||||
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
|
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
|
||||||
|
@ -28,6 +29,7 @@ const tenancy = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenantId = getTenantIDFromCtx(ctx, tenantOpts)
|
const tenantId = getTenantIDFromCtx(ctx, tenantOpts)
|
||||||
|
ctx.set(Headers.TENANT_ID, tenantId as string)
|
||||||
return doInTenant(tenantId, next)
|
return doInTenant(tenantId, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,6 +214,7 @@ export const getTenantIDFromCtx = (
|
||||||
// e.g. tenant.budibase.app or tenant.local.com
|
// e.g. tenant.budibase.app or tenant.local.com
|
||||||
const requestHost = ctx.host
|
const requestHost = ctx.host
|
||||||
// parse the tenant id from the difference
|
// parse the tenant id from the difference
|
||||||
|
if (requestHost.includes(platformHost)) {
|
||||||
const tenantId = requestHost.substring(
|
const tenantId = requestHost.substring(
|
||||||
0,
|
0,
|
||||||
requestHost.indexOf(`.${platformHost}`)
|
requestHost.indexOf(`.${platformHost}`)
|
||||||
|
@ -222,6 +223,24 @@ export const getTenantIDFromCtx = (
|
||||||
return tenantId
|
return tenantId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAllowed(TenantResolutionStrategy.PATH)) {
|
||||||
|
// params - have to parse manually due to koa-router not run yet
|
||||||
|
const match = ctx.matched.find(
|
||||||
|
(m: any) => !!m.paramNames.find((p: any) => p.name === "tenantId")
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
const params = match.params(
|
||||||
|
ctx.originalUrl,
|
||||||
|
match.captures(ctx.originalUrl),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
if (params.tenantId) {
|
||||||
|
return params.tenantId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!opts.allowNoTenant) {
|
if (!opts.allowNoTenant) {
|
||||||
ctx.throw(403, "Tenant id not set")
|
ctx.throw(403, "Tenant id not set")
|
||||||
|
|
|
@ -8,7 +8,12 @@ import userCache from "./cache/user"
|
||||||
import { getSessionsForUser, invalidateSessions } from "./security/sessions"
|
import { getSessionsForUser, invalidateSessions } from "./security/sessions"
|
||||||
import * as events from "./events"
|
import * as events from "./events"
|
||||||
import tenancy from "./tenancy"
|
import tenancy from "./tenancy"
|
||||||
import { App, BBContext, TenantResolutionStrategy } from "@budibase/types"
|
import {
|
||||||
|
App,
|
||||||
|
BBContext,
|
||||||
|
PlatformLogoutOpts,
|
||||||
|
TenantResolutionStrategy,
|
||||||
|
} from "@budibase/types"
|
||||||
import { SetOption } from "cookies"
|
import { SetOption } from "cookies"
|
||||||
|
|
||||||
const APP_PREFIX = DocumentType.APP + SEPARATOR
|
const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||||
|
@ -189,12 +194,6 @@ export const getBuildersCount = async () => {
|
||||||
return builders.length
|
return builders.length
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlatformLogoutOpts {
|
|
||||||
ctx: BBContext
|
|
||||||
userId: string
|
|
||||||
keepActiveSession: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a user out from budibase. Re-used across account portal and builder.
|
* Logs a user out from budibase. Re-used across account portal and builder.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * as mocks from "./mocks"
|
export * as mocks from "./mocks"
|
||||||
export * as structures from "./structures"
|
export * as structures from "./structures"
|
||||||
|
export { generator } from "./structures"
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
export const getAccount = jest.fn()
|
export const getAccount = jest.fn()
|
||||||
export const getAccountByTenantId = jest.fn()
|
export const getAccountByTenantId = jest.fn()
|
||||||
|
export const getStatus = jest.fn()
|
||||||
|
|
||||||
jest.mock("../../../src/cloud/accounts", () => ({
|
jest.mock("../../../src/cloud/accounts", () => ({
|
||||||
getAccount,
|
getAccount,
|
||||||
getAccountByTenantId,
|
getAccountByTenantId,
|
||||||
|
getStatus,
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -1,23 +1,29 @@
|
||||||
import { generator, uuid } from "."
|
import { generator, uuid } from "."
|
||||||
import { AuthType, CloudAccount, Hosting } from "@budibase/types"
|
|
||||||
import * as db from "../../../src/db/utils"
|
import * as db from "../../../src/db/utils"
|
||||||
|
import { Account, AuthType, CloudAccount, Hosting } from "@budibase/types"
|
||||||
|
|
||||||
export const cloudAccount = (): CloudAccount => {
|
export const account = (): Account => {
|
||||||
return {
|
return {
|
||||||
accountId: uuid(),
|
accountId: uuid(),
|
||||||
|
tenantId: generator.word(),
|
||||||
|
email: generator.email(),
|
||||||
|
tenantName: generator.word(),
|
||||||
|
hosting: Hosting.SELF,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
verified: true,
|
verified: true,
|
||||||
verificationSent: true,
|
verificationSent: true,
|
||||||
tier: "",
|
tier: "FREE", // DEPRECATED
|
||||||
email: generator.email(),
|
|
||||||
tenantId: generator.word(),
|
|
||||||
hosting: Hosting.CLOUD,
|
|
||||||
authType: AuthType.PASSWORD,
|
authType: AuthType.PASSWORD,
|
||||||
password: generator.word(),
|
|
||||||
tenantName: generator.word(),
|
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
size: "10+",
|
size: "10+",
|
||||||
profession: "Software Engineer",
|
profession: "Software Engineer",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cloudAccount = (): CloudAccount => {
|
||||||
|
return {
|
||||||
|
...account(),
|
||||||
|
hosting: Hosting.CLOUD,
|
||||||
budibaseUserId: db.generateGlobalUserID(),
|
budibaseUserId: db.generateGlobalUserID(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./info"
|
export * from "./info"
|
||||||
export * from "./users"
|
export * from "./users"
|
||||||
export * from "./accounts"
|
export * from "./accounts"
|
||||||
|
export * from "./tenants"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Document } from "../document"
|
||||||
|
|
||||||
|
export interface Tenants extends Document {
|
||||||
|
tenantIds: string[]
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { BBContext } from "./koa"
|
||||||
|
|
||||||
export interface AuthToken {
|
export interface AuthToken {
|
||||||
userId: string
|
userId: string
|
||||||
tenantId: string
|
tenantId: string
|
||||||
|
@ -25,3 +27,9 @@ export interface SessionKey {
|
||||||
export interface ScannedSession {
|
export interface ScannedSession {
|
||||||
value: Session
|
value: Session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlatformLogoutOpts {
|
||||||
|
ctx: BBContext
|
||||||
|
userId: string
|
||||||
|
keepActiveSession: boolean
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,22 @@
|
||||||
export interface EndpointMatcher {
|
export interface EndpointMatcher {
|
||||||
|
/**
|
||||||
|
* The HTTP Path. e.g. /api/things/:thingId
|
||||||
|
*/
|
||||||
route: string
|
route: string
|
||||||
|
/**
|
||||||
|
* The HTTP Verb. e.g. GET, POST, etc.
|
||||||
|
* ALL is also accepted to cover all verbs.
|
||||||
|
*/
|
||||||
method: string
|
method: string
|
||||||
|
/**
|
||||||
|
* The route must match exactly - not just begins with
|
||||||
|
*/
|
||||||
|
strict?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegexMatcher {
|
||||||
|
regex: RegExp
|
||||||
|
method: string
|
||||||
|
strict: boolean
|
||||||
|
route: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,4 +9,5 @@ export enum TenantResolutionStrategy {
|
||||||
HEADER = "header",
|
HEADER = "header",
|
||||||
QUERY = "query",
|
QUERY = "query",
|
||||||
SUBDOMAIN = "subdomain",
|
SUBDOMAIN = "subdomain",
|
||||||
|
PATH = "path",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
const env = require("../src/environment")
|
const env = require("../src/environment")
|
||||||
|
|
||||||
env._set("SELF_HOSTED", "1")
|
env._set("SELF_HOSTED", "0")
|
||||||
env._set("NODE_ENV", "jest")
|
env._set("NODE_ENV", "jest")
|
||||||
env._set("JWT_SECRET", "test-jwtsecret")
|
env._set("JWT_SECRET", "test-jwtsecret")
|
||||||
env._set("LOG_LEVEL", "silent")
|
env._set("LOG_LEVEL", "silent")
|
||||||
env._set("MULTI_TENANCY", true)
|
env._set("MULTI_TENANCY", true)
|
||||||
|
env._set("PLATFORM_URL", "http://localhost:10000")
|
||||||
|
env._set("INTERNAL_API_KEY", "test")
|
||||||
|
env._set("DISABLE_ACCOUNT_PORTAL", false)
|
||||||
|
|
||||||
const { mocks } = require("@budibase/backend-core/tests")
|
const { mocks } = require("@budibase/backend-core/tests")
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const env = require("../../../environment")
|
import { BBContext } from "@budibase/types"
|
||||||
|
import env from "../../../environment"
|
||||||
|
|
||||||
exports.fetch = async ctx => {
|
export const fetch = async (ctx: BBContext) => {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
multiTenancy: !!env.MULTI_TENANCY,
|
multiTenancy: !!env.MULTI_TENANCY,
|
||||||
cloud: !env.SELF_HOSTED,
|
cloud: !env.SELF_HOSTED,
|
|
@ -1,7 +1,8 @@
|
||||||
const accounts = require("@budibase/backend-core/accounts")
|
import { accounts } from "@budibase/backend-core"
|
||||||
const env = require("../../../environment")
|
import env from "../../../environment"
|
||||||
|
import { BBContext } from "@budibase/types"
|
||||||
|
|
||||||
exports.fetch = async ctx => {
|
export const fetch = async (ctx: BBContext) => {
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
const status = await accounts.getStatus()
|
const status = await accounts.getStatus()
|
||||||
ctx.body = status
|
ctx.body = status
|
|
@ -1,60 +1,17 @@
|
||||||
const { StaticDatabases, doWithDB } = require("@budibase/backend-core/db")
|
import { BBContext } from "@budibase/types"
|
||||||
const { getTenantId } = require("@budibase/backend-core/tenancy")
|
import { deprovisioning } from "@budibase/backend-core"
|
||||||
const { deleteTenant } = require("@budibase/backend-core/deprovision")
|
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
|
|
||||||
export const exists = async (ctx: any) => {
|
const _delete = async (ctx: BBContext) => {
|
||||||
const tenantId = ctx.request.params
|
const user = ctx.user!
|
||||||
ctx.body = {
|
const tenantId = ctx.params.tenantId
|
||||||
exists: await doWithDB(
|
|
||||||
StaticDatabases.PLATFORM_INFO.name,
|
|
||||||
async (db: any) => {
|
|
||||||
let exists = false
|
|
||||||
try {
|
|
||||||
const tenantsDoc = await db.get(
|
|
||||||
StaticDatabases.PLATFORM_INFO.docs.tenants
|
|
||||||
)
|
|
||||||
if (tenantsDoc) {
|
|
||||||
exists = tenantsDoc.tenantIds.indexOf(tenantId) !== -1
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// if error it doesn't exist
|
|
||||||
}
|
|
||||||
return exists
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetch = async (ctx: any) => {
|
if (tenantId !== user.tenantId) {
|
||||||
ctx.body = await doWithDB(
|
ctx.throw(403, "Tenant ID does not match current user")
|
||||||
StaticDatabases.PLATFORM_INFO.name,
|
|
||||||
async (db: any) => {
|
|
||||||
let tenants = []
|
|
||||||
try {
|
|
||||||
const tenantsDoc = await db.get(
|
|
||||||
StaticDatabases.PLATFORM_INFO.docs.tenants
|
|
||||||
)
|
|
||||||
if (tenantsDoc) {
|
|
||||||
tenants = tenantsDoc.tenantIds
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// if error it doesn't exist
|
|
||||||
}
|
|
||||||
return tenants
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const _delete = async (ctx: any) => {
|
|
||||||
const tenantId = getTenantId()
|
|
||||||
|
|
||||||
if (ctx.params.tenantId !== tenantId) {
|
|
||||||
ctx.throw(403, "Unauthorized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteTenant(tenantId)
|
await deprovisioning.deleteTenant(tenantId)
|
||||||
await quotas.bustCache()
|
await quotas.bustCache()
|
||||||
ctx.status = 204
|
ctx.status = 204
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -7,11 +7,12 @@ import { errors, auth, middleware } from "@budibase/backend-core"
|
||||||
import { APIError } from "@budibase/types"
|
import { APIError } from "@budibase/types"
|
||||||
|
|
||||||
const PUBLIC_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
// old deprecated endpoints kept for backwards compat
|
// deprecated single tenant sso callback
|
||||||
{
|
{
|
||||||
route: "/api/admin/auth/google/callback",
|
route: "/api/admin/auth/google/callback",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
// deprecated single tenant sso callback
|
||||||
{
|
{
|
||||||
route: "/api/admin/auth/oidc/callback",
|
route: "/api/admin/auth/oidc/callback",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -51,10 +52,12 @@ const PUBLIC_ENDPOINTS = [
|
||||||
route: "api/system/status",
|
route: "api/system/status",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
// TODO: This should be an internal api
|
||||||
{
|
{
|
||||||
route: "/api/global/users/tenant/:id",
|
route: "/api/global/users/tenant/:id",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
// TODO: This should be an internal api
|
||||||
{
|
{
|
||||||
route: "/api/system/restored",
|
route: "/api/system/restored",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -62,17 +65,37 @@ const PUBLIC_ENDPOINTS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const NO_TENANCY_ENDPOINTS = [
|
const NO_TENANCY_ENDPOINTS = [
|
||||||
...PUBLIC_ENDPOINTS,
|
// system endpoints are not specific to any tenant
|
||||||
{
|
{
|
||||||
route: "/api/system",
|
route: "/api/system",
|
||||||
method: "ALL",
|
method: "ALL",
|
||||||
},
|
},
|
||||||
|
// tenant is determined in request body
|
||||||
|
// used for creating the tenant
|
||||||
{
|
{
|
||||||
route: "/api/global/users/self",
|
route: "/api/global/users/init",
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
// deprecated single tenant sso callback
|
||||||
|
{
|
||||||
|
route: "/api/admin/auth/google/callback",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
// deprecated single tenant sso callback
|
||||||
{
|
{
|
||||||
route: "/api/global/self",
|
route: "/api/admin/auth/oidc/callback",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
// tenant is determined from code in redis
|
||||||
|
{
|
||||||
|
route: "/api/global/users/invite/accept",
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
// global user search - no tenancy
|
||||||
|
// :id is user id
|
||||||
|
// TODO: this should really be `/api/system/users/:id`
|
||||||
|
{
|
||||||
|
route: "/api/global/users/tenant/:id",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,7 +2,6 @@ const Router = require("@koa/router")
|
||||||
const authController = require("../../controllers/global/auth")
|
const authController = require("../../controllers/global/auth")
|
||||||
const { joiValidator } = require("@budibase/backend-core/auth")
|
const { joiValidator } = require("@budibase/backend-core/auth")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
const { updateTenantId } = require("@budibase/backend-core/tenancy")
|
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
|
||||||
|
@ -29,43 +28,28 @@ function buildResetUpdateValidation() {
|
||||||
}).required().unknown(false))
|
}).required().unknown(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTenant(ctx, next) {
|
|
||||||
if (ctx.params) {
|
|
||||||
updateTenantId(ctx.params.tenantId)
|
|
||||||
}
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
router
|
router
|
||||||
.post(
|
.post(
|
||||||
"/api/global/auth/:tenantId/login",
|
"/api/global/auth/:tenantId/login",
|
||||||
buildAuthValidation(),
|
buildAuthValidation(),
|
||||||
updateTenant,
|
|
||||||
authController.authenticate
|
authController.authenticate
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/global/auth/:tenantId/reset",
|
"/api/global/auth/:tenantId/reset",
|
||||||
buildResetValidation(),
|
buildResetValidation(),
|
||||||
updateTenant,
|
|
||||||
authController.reset
|
authController.reset
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/global/auth/:tenantId/reset/update",
|
"/api/global/auth/:tenantId/reset/update",
|
||||||
buildResetUpdateValidation(),
|
buildResetUpdateValidation(),
|
||||||
updateTenant,
|
|
||||||
authController.resetUpdate
|
authController.resetUpdate
|
||||||
)
|
)
|
||||||
.post("/api/global/auth/logout", authController.logout)
|
.post("/api/global/auth/logout", authController.logout)
|
||||||
.post("/api/global/auth/init", authController.setInitInfo)
|
.post("/api/global/auth/init", authController.setInitInfo)
|
||||||
.get("/api/global/auth/init", authController.getInitInfo)
|
.get("/api/global/auth/init", authController.getInitInfo)
|
||||||
.get(
|
.get("/api/global/auth/:tenantId/google", authController.googlePreAuth)
|
||||||
"/api/global/auth/:tenantId/google",
|
|
||||||
updateTenant,
|
|
||||||
authController.googlePreAuth
|
|
||||||
)
|
|
||||||
.get(
|
.get(
|
||||||
"/api/global/auth/:tenantId/datasource/:provider",
|
"/api/global/auth/:tenantId/datasource/:provider",
|
||||||
updateTenant,
|
|
||||||
authController.datasourcePreAuth
|
authController.datasourcePreAuth
|
||||||
)
|
)
|
||||||
// single tenancy endpoint
|
// single tenancy endpoint
|
||||||
|
@ -75,29 +59,19 @@ router
|
||||||
authController.datasourceAuth
|
authController.datasourceAuth
|
||||||
)
|
)
|
||||||
// multi-tenancy endpoint
|
// multi-tenancy endpoint
|
||||||
.get(
|
.get("/api/global/auth/:tenantId/google/callback", authController.googleAuth)
|
||||||
"/api/global/auth/:tenantId/google/callback",
|
|
||||||
updateTenant,
|
|
||||||
authController.googleAuth
|
|
||||||
)
|
|
||||||
.get(
|
.get(
|
||||||
"/api/global/auth/:tenantId/datasource/:provider/callback",
|
"/api/global/auth/:tenantId/datasource/:provider/callback",
|
||||||
updateTenant,
|
|
||||||
authController.datasourceAuth
|
authController.datasourceAuth
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/api/global/auth/:tenantId/oidc/configs/:configId",
|
"/api/global/auth/:tenantId/oidc/configs/:configId",
|
||||||
updateTenant,
|
|
||||||
authController.oidcPreAuth
|
authController.oidcPreAuth
|
||||||
)
|
)
|
||||||
// single tenancy endpoint
|
// single tenancy endpoint
|
||||||
.get("/api/global/auth/oidc/callback", authController.oidcAuth)
|
.get("/api/global/auth/oidc/callback", authController.oidcAuth)
|
||||||
// multi-tenancy endpoint
|
// multi-tenancy endpoint
|
||||||
.get(
|
.get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth)
|
||||||
"/api/global/auth/:tenantId/oidc/callback",
|
|
||||||
updateTenant,
|
|
||||||
authController.oidcAuth
|
|
||||||
)
|
|
||||||
// deprecated - used by the default system before tenancy
|
// deprecated - used by the default system before tenancy
|
||||||
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
||||||
.get("/api/admin/auth/oidc/callback", authController.oidcAuth)
|
.get("/api/admin/auth/oidc/callback", authController.oidcAuth)
|
||||||
|
|
|
@ -235,7 +235,7 @@ describe("configs", () => {
|
||||||
expect(events.org.nameUpdated).toBeCalledTimes(1)
|
expect(events.org.nameUpdated).toBeCalledTimes(1)
|
||||||
expect(events.org.logoUpdated).toBeCalledTimes(1)
|
expect(events.org.logoUpdated).toBeCalledTimes(1)
|
||||||
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
|
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
|
||||||
config.modeAccount()
|
config.modeCloud()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ describe("configs", () => {
|
||||||
expect(events.org.nameUpdated).toBeCalledTimes(1)
|
expect(events.org.nameUpdated).toBeCalledTimes(1)
|
||||||
expect(events.org.logoUpdated).toBeCalledTimes(1)
|
expect(events.org.logoUpdated).toBeCalledTimes(1)
|
||||||
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
|
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
|
||||||
config.modeAccount()
|
config.modeCloud()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
|
||||||
|
describe("/api/global/license", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /api/global/license/activate", () => {})
|
||||||
|
|
||||||
|
describe("POST /api/global/license/refresh", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/license/info", () => {})
|
||||||
|
|
||||||
|
describe("DELETE /api/global/license/info", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/license/usage", () => {})
|
||||||
|
})
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
|
||||||
|
describe("/api/global/roles", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /api/global/roles", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/roles/:appId", () => {})
|
||||||
|
|
||||||
|
describe("DELETE /api/global/roles/:appId", () => {})
|
||||||
|
})
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
|
||||||
|
describe("/api/global/template", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /api/global/template/definitions", () => {})
|
||||||
|
|
||||||
|
describe("POST /api/global/template", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/template", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/template/:type", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/template/:ownerId", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/template/:id", () => {})
|
||||||
|
|
||||||
|
describe("DELETE /api/global/template/:id/:rev", () => {})
|
||||||
|
})
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
|
||||||
|
describe("/api/global/workspaces", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /api/global/workspaces", () => {})
|
||||||
|
|
||||||
|
describe("DELETE /api/global/workspaces/:id", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/workspaces", () => {})
|
||||||
|
|
||||||
|
describe("GET /api/global/workspaces/:id", () => {})
|
||||||
|
})
|
|
@ -1,8 +0,0 @@
|
||||||
const Router = require("@koa/router")
|
|
||||||
const controller = require("../../controllers/system/environment")
|
|
||||||
|
|
||||||
const router = new Router()
|
|
||||||
|
|
||||||
router.get("/api/system/environment", controller.fetch)
|
|
||||||
|
|
||||||
module.exports = router
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import Router from "@koa/router"
|
||||||
|
import * as controller from "../../controllers/system/environment"
|
||||||
|
|
||||||
|
const router = new Router()
|
||||||
|
|
||||||
|
router.get("/api/system/environment", controller.fetch)
|
||||||
|
|
||||||
|
export default router
|
|
@ -1,8 +0,0 @@
|
||||||
const Router = require("@koa/router")
|
|
||||||
const controller = require("../../controllers/system/status")
|
|
||||||
|
|
||||||
const router = new Router()
|
|
||||||
|
|
||||||
router.get("/api/system/status", controller.fetch)
|
|
||||||
|
|
||||||
module.exports = router
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import Router from "@koa/router"
|
||||||
|
import * as controller from "../../controllers/system/status"
|
||||||
|
|
||||||
|
const router = new Router()
|
||||||
|
|
||||||
|
router.get("/api/system/status", controller.fetch)
|
||||||
|
|
||||||
|
export default router
|
|
@ -1,12 +0,0 @@
|
||||||
const Router = require("@koa/router")
|
|
||||||
const controller = require("../../controllers/system/tenants")
|
|
||||||
const { adminOnly } = require("@budibase/backend-core/auth")
|
|
||||||
|
|
||||||
const router = new Router()
|
|
||||||
|
|
||||||
router
|
|
||||||
.get("/api/system/tenants/:tenantId/exists", controller.exists)
|
|
||||||
.get("/api/system/tenants", adminOnly, controller.fetch)
|
|
||||||
.delete("/api/system/tenants/:tenantId", adminOnly, controller.delete)
|
|
||||||
|
|
||||||
module.exports = router
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Router from "@koa/router"
|
||||||
|
import * as controller from "../../controllers/system/tenants"
|
||||||
|
import { middleware } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
const router = new Router()
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/api/system/tenants/:tenantId",
|
||||||
|
middleware.adminOnly,
|
||||||
|
controller.delete
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
|
||||||
|
describe("/api/system/environment", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /api/system/environment", () => {
|
||||||
|
it("returns the expected environment", async () => {
|
||||||
|
const env = await api.environment.getEnvironment()
|
||||||
|
expect(env.body).toEqual({
|
||||||
|
cloud: true,
|
||||||
|
disableAccountPortal: false,
|
||||||
|
isDev: false,
|
||||||
|
multiTenancy: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,64 @@
|
||||||
|
const migrateFn = jest.fn()
|
||||||
|
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
|
||||||
|
jest.mock("../../../../migrations", () => {
|
||||||
|
return {
|
||||||
|
...jest.requireActual("../../../../migrations"),
|
||||||
|
migrate: migrateFn,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("/api/system/migrations", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /api/system/migrations/run", () => {
|
||||||
|
it("fails with no internal api key", async () => {
|
||||||
|
const res = await api.migrations.runMigrations({
|
||||||
|
headers: {},
|
||||||
|
status: 403,
|
||||||
|
})
|
||||||
|
expect(res.text).toBe("Unauthorized - no public worker access")
|
||||||
|
expect(migrateFn).toBeCalledTimes(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("runs migrations", async () => {
|
||||||
|
const res = await api.migrations.runMigrations()
|
||||||
|
expect(res.text).toBe("OK")
|
||||||
|
expect(migrateFn).toBeCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DELETE /api/system/migrations/definitions", () => {
|
||||||
|
it("fails with no internal api key", async () => {
|
||||||
|
const res = await api.migrations.getMigrationDefinitions({
|
||||||
|
headers: {},
|
||||||
|
status: 403,
|
||||||
|
})
|
||||||
|
expect(res.text).toBe("Unauthorized - no public worker access")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns definitions", async () => {
|
||||||
|
const res = await api.migrations.getMigrationDefinitions()
|
||||||
|
expect(res.body).toEqual([
|
||||||
|
{
|
||||||
|
name: "global_info_sync_users",
|
||||||
|
type: "global",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
|
||||||
|
describe("/api/system/restore", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /api/global/restore", () => {
|
||||||
|
it("doesn't allow restore in cloud", async () => {
|
||||||
|
const res = await api.restore.restored({ status: 405 })
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
message: "This operation is not allowed in cloud.",
|
||||||
|
status: 405,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("restores in self host", async () => {
|
||||||
|
config.modeSelf()
|
||||||
|
const res = await api.restore.restored()
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
message: "System prepared after restore.",
|
||||||
|
})
|
||||||
|
config.modeCloud()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
import { accounts } from "@budibase/backend-core"
|
||||||
|
import { mocks } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
|
describe("/api/system/status", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /api/system/status", () => {
|
||||||
|
it("returns status in self host", async () => {
|
||||||
|
config.modeSelf()
|
||||||
|
const res = await api.status.getStatus()
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
health: {
|
||||||
|
passing: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(accounts.getStatus).toBeCalledTimes(0)
|
||||||
|
config.modeCloud()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns status in cloud", async () => {
|
||||||
|
const value = {
|
||||||
|
health: {
|
||||||
|
passing: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mocks.accounts.getStatus.mockReturnValueOnce(value)
|
||||||
|
|
||||||
|
const res = await api.status.getStatus()
|
||||||
|
|
||||||
|
expect(accounts.getStatus).toBeCalledTimes(1)
|
||||||
|
expect(res.body).toEqual(value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
|
|
||||||
|
describe("/api/global/workspaces", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DELETE /api/system/tenants/:tenantId", () => {
|
||||||
|
it("allows deleting the current tenant", async () => {
|
||||||
|
const user = await config.createTenant()
|
||||||
|
await config.createSession(user)
|
||||||
|
const res = await api.tenants.delete(user.tenantId, {
|
||||||
|
headers: config.authHeaders(user),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects deleting another tenant", () => {})
|
||||||
|
|
||||||
|
it("requires admin", () => {})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { TestConfiguration, API, structures } from "../../tests"
|
||||||
|
import { constants } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
describe("tenancy middleware", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get tenant id from user", async () => {
|
||||||
|
const user = await config.createTenant()
|
||||||
|
await config.createSession(user)
|
||||||
|
const res = await api.self.getSelf(user)
|
||||||
|
expect(res.headers[constants.Headers.TENANT_ID]).toBe(user.tenantId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get tenant id from header", async () => {
|
||||||
|
const tenantId = structures.uuid()
|
||||||
|
const headers = {
|
||||||
|
[constants.Headers.TENANT_ID]: tenantId,
|
||||||
|
}
|
||||||
|
const res = await config.request
|
||||||
|
.get(`/api/global/configs/checklist`)
|
||||||
|
.set(headers)
|
||||||
|
expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get tenant id from query param", async () => {
|
||||||
|
const tenantId = structures.uuid()
|
||||||
|
const res = await config.request.get(
|
||||||
|
`/api/global/configs/checklist?tenantId=${tenantId}`
|
||||||
|
)
|
||||||
|
expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get tenant id from subdomain", async () => {
|
||||||
|
const tenantId = structures.uuid()
|
||||||
|
const headers = {
|
||||||
|
host: `${tenantId}.localhost:10000`,
|
||||||
|
}
|
||||||
|
const res = await config.request
|
||||||
|
.get(`/api/global/configs/checklist`)
|
||||||
|
.set(headers)
|
||||||
|
expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get tenant id from path variable", async () => {
|
||||||
|
const user = await config.createTenant()
|
||||||
|
const res = await config.request
|
||||||
|
.post(`/api/global/auth/${user.tenantId}/login`)
|
||||||
|
.send({
|
||||||
|
username: user.email,
|
||||||
|
password: user.password,
|
||||||
|
})
|
||||||
|
expect(res.headers[constants.Headers.TENANT_ID]).toBe(user.tenantId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw when no tenant id is found", async () => {
|
||||||
|
const res = await config.request.get(`/api/global/configs/checklist`)
|
||||||
|
expect(res.status).toBe(403)
|
||||||
|
expect(res.text).toBe("Tenant id not set")
|
||||||
|
expect(res.headers[constants.Headers.TENANT_ID]).toBe(undefined)
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,6 +2,7 @@ import "./mocks"
|
||||||
import dbConfig from "../db"
|
import dbConfig from "../db"
|
||||||
dbConfig.init()
|
dbConfig.init()
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { env as coreEnv } from "@budibase/backend-core"
|
||||||
import controllers from "./controllers"
|
import controllers from "./controllers"
|
||||||
const supertest = require("supertest")
|
const supertest = require("supertest")
|
||||||
import { Configs } from "../constants"
|
import { Configs } from "../constants"
|
||||||
|
@ -12,13 +13,13 @@ import {
|
||||||
Headers,
|
Headers,
|
||||||
sessions,
|
sessions,
|
||||||
auth,
|
auth,
|
||||||
|
constants,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures"
|
import structures, { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures"
|
||||||
import structures from "./structures"
|
|
||||||
import { CreateUserResponse, User, AuthToken } from "@budibase/types"
|
import { CreateUserResponse, User, AuthToken } from "@budibase/types"
|
||||||
|
|
||||||
enum Mode {
|
enum Mode {
|
||||||
ACCOUNT = "account",
|
CLOUD = "cloud",
|
||||||
SELF = "self",
|
SELF = "self",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,11 +32,11 @@ class TestConfiguration {
|
||||||
constructor(
|
constructor(
|
||||||
opts: { openServer: boolean; mode: Mode } = {
|
opts: { openServer: boolean; mode: Mode } = {
|
||||||
openServer: true,
|
openServer: true,
|
||||||
mode: Mode.ACCOUNT,
|
mode: Mode.CLOUD,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (opts.mode === Mode.ACCOUNT) {
|
if (opts.mode === Mode.CLOUD) {
|
||||||
this.modeAccount()
|
this.modeCloud()
|
||||||
} else if (opts.mode === Mode.SELF) {
|
} else if (opts.mode === Mode.SELF) {
|
||||||
this.modeSelf()
|
this.modeSelf()
|
||||||
}
|
}
|
||||||
|
@ -54,20 +55,24 @@ class TestConfiguration {
|
||||||
|
|
||||||
// MODES
|
// MODES
|
||||||
|
|
||||||
modeAccount = () => {
|
setMultiTenancy = (value: boolean) => {
|
||||||
env.SELF_HOSTED = false
|
env._set("MULTI_TENANCY", value)
|
||||||
// @ts-ignore
|
coreEnv._set("MULTI_TENANCY", value)
|
||||||
env.MULTI_TENANCY = true
|
}
|
||||||
// @ts-ignore
|
|
||||||
env.DISABLE_ACCOUNT_PORTAL = false
|
setSelfHosted = (value: boolean) => {
|
||||||
|
env._set("SELF_HOSTED", value)
|
||||||
|
coreEnv._set("SELF_HOSTED", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
modeCloud = () => {
|
||||||
|
this.setSelfHosted(false)
|
||||||
|
this.setMultiTenancy(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
modeSelf = () => {
|
modeSelf = () => {
|
||||||
env.SELF_HOSTED = true
|
this.setSelfHosted(true)
|
||||||
// @ts-ignore
|
this.setMultiTenancy(false)
|
||||||
env.MULTI_TENANCY = false
|
|
||||||
// @ts-ignore
|
|
||||||
env.DISABLE_ACCOUNT_PORTAL = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UTILS
|
// UTILS
|
||||||
|
@ -114,6 +119,16 @@ class TestConfiguration {
|
||||||
|
|
||||||
// TENANCY
|
// TENANCY
|
||||||
|
|
||||||
|
createTenant = async (tenantId?: string): Promise<User> => {
|
||||||
|
// create user / new tenant
|
||||||
|
if (!tenantId) {
|
||||||
|
tenantId = structures.uuid()
|
||||||
|
}
|
||||||
|
return tenancy.doInTenant(tenantId, async () => {
|
||||||
|
return this.createUser()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
getTenantId() {
|
getTenantId() {
|
||||||
try {
|
try {
|
||||||
return tenancy.getTenantId()
|
return tenancy.getTenantId()
|
||||||
|
@ -179,6 +194,10 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internalAPIHeaders() {
|
||||||
|
return { [constants.Headers.API_KEY]: env.INTERNAL_API_KEY }
|
||||||
|
}
|
||||||
|
|
||||||
async getUser(email: string): Promise<User> {
|
async getUser(email: string): Promise<User> {
|
||||||
return tenancy.doInTenant(this.getTenantId(), () => {
|
return tenancy.doInTenant(this.getTenantId(), () => {
|
||||||
return users.getGlobalUserByEmail(email)
|
return users.getGlobalUserByEmail(email)
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
export interface TestAPIOpts {
|
||||||
|
headers?: any
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TestAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
protected constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
export class EnvironmentAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnvironment = () => {
|
||||||
|
return this.request
|
||||||
|
.get(`/api/system/environment`)
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,11 @@ import { ConfigAPI } from "./configs"
|
||||||
import { EmailAPI } from "./email"
|
import { EmailAPI } from "./email"
|
||||||
import { SelfAPI } from "./self"
|
import { SelfAPI } from "./self"
|
||||||
import { UserAPI } from "./users"
|
import { UserAPI } from "./users"
|
||||||
|
import { EnvironmentAPI } from "./environment"
|
||||||
|
import { MigrationAPI } from "./migrations"
|
||||||
|
import { StatusAPI } from "./status"
|
||||||
|
import { RestoreAPI } from "./restore"
|
||||||
|
import { TenantAPI } from "./tenants"
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
accounts: AccountAPI
|
accounts: AccountAPI
|
||||||
|
@ -13,6 +18,11 @@ export default class API {
|
||||||
emails: EmailAPI
|
emails: EmailAPI
|
||||||
self: SelfAPI
|
self: SelfAPI
|
||||||
users: UserAPI
|
users: UserAPI
|
||||||
|
environment: EnvironmentAPI
|
||||||
|
migrations: MigrationAPI
|
||||||
|
status: StatusAPI
|
||||||
|
restore: RestoreAPI
|
||||||
|
tenants: TenantAPI
|
||||||
|
|
||||||
constructor(config: TestConfiguration) {
|
constructor(config: TestConfiguration) {
|
||||||
this.accounts = new AccountAPI(config)
|
this.accounts = new AccountAPI(config)
|
||||||
|
@ -21,5 +31,10 @@ export default class API {
|
||||||
this.emails = new EmailAPI(config)
|
this.emails = new EmailAPI(config)
|
||||||
this.self = new SelfAPI(config)
|
this.self = new SelfAPI(config)
|
||||||
this.users = new UserAPI(config)
|
this.users = new UserAPI(config)
|
||||||
|
this.environment = new EnvironmentAPI(config)
|
||||||
|
this.migrations = new MigrationAPI(config)
|
||||||
|
this.status = new StatusAPI(config)
|
||||||
|
this.restore = new RestoreAPI(config)
|
||||||
|
this.tenants = new TenantAPI(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
import { TestAPI, TestAPIOpts } from "./base"
|
||||||
|
|
||||||
|
export class MigrationAPI extends TestAPI {
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
super(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
runMigrations = (opts?: TestAPIOpts) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/system/migrations/run`)
|
||||||
|
.set(opts?.headers ? opts.headers : this.config.internalAPIHeaders())
|
||||||
|
.expect(opts?.status ? opts.status : 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
getMigrationDefinitions = (opts?: TestAPIOpts) => {
|
||||||
|
return this.request
|
||||||
|
.get(`/api/system/migrations/definitions`)
|
||||||
|
.set(opts?.headers ? opts.headers : this.config.internalAPIHeaders())
|
||||||
|
.expect(opts?.status ? opts.status : 200)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
import { TestAPI, TestAPIOpts } from "./base"
|
||||||
|
|
||||||
|
export class RestoreAPI extends TestAPI {
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
super(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
restored = (opts?: TestAPIOpts) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/system/restored`)
|
||||||
|
.expect(opts?.status ? opts.status : 200)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,4 +18,12 @@ export class SelfAPI {
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSelf = (user: User) => {
|
||||||
|
return this.request
|
||||||
|
.get(`/api/global/self`)
|
||||||
|
.set(this.config.authHeaders(user))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
export class StatusAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus = () => {
|
||||||
|
return this.request.get(`/api/system/status`).expect(200)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
import { TestAPI, TestAPIOpts } from "./base"
|
||||||
|
|
||||||
|
export class TenantAPI extends TestAPI {
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
super(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = (tenantId: string, opts?: TestAPIOpts) => {
|
||||||
|
return this.request
|
||||||
|
.delete(`/api/system/tenants/${tenantId}`)
|
||||||
|
.set(opts?.headers)
|
||||||
|
.expect(opts?.status ? opts.status : 200)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,14 @@
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
import TestConfiguration from "./TestConfiguration"
|
import TestConfiguration from "./TestConfiguration"
|
||||||
import structures from "./structures"
|
import structures from "./structures"
|
||||||
import mocks from "./mocks"
|
import mocks from "./mocks"
|
||||||
import API from "./api"
|
import API from "./api"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
|
||||||
const pkg = {
|
const pkg = {
|
||||||
structures,
|
structures,
|
||||||
|
generator,
|
||||||
|
uuid,
|
||||||
TENANT_1: structures.TENANT_1,
|
TENANT_1: structures.TENANT_1,
|
||||||
mocks,
|
mocks,
|
||||||
TestConfiguration,
|
TestConfiguration,
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Account, AuthType, Hosting, CloudAccount } from "@budibase/types"
|
|
||||||
import { v4 as uuid } from "uuid"
|
|
||||||
import { utils } from "@budibase/backend-core"
|
|
||||||
|
|
||||||
export const account = (): Account => {
|
|
||||||
return {
|
|
||||||
email: `${uuid()}@test.com`,
|
|
||||||
tenantId: utils.newid(),
|
|
||||||
hosting: Hosting.SELF,
|
|
||||||
authType: AuthType.SSO,
|
|
||||||
accountId: uuid(),
|
|
||||||
createdAt: Date.now(),
|
|
||||||
verified: true,
|
|
||||||
verificationSent: true,
|
|
||||||
tier: "FREE",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const cloudAccount = (): CloudAccount => {
|
|
||||||
return {
|
|
||||||
...account(),
|
|
||||||
budibaseUserId: uuid(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,18 @@
|
||||||
|
import { structures } from "@budibase/backend-core/tests"
|
||||||
import configs from "./configs"
|
import configs from "./configs"
|
||||||
import * as users from "./users"
|
import * as users from "./users"
|
||||||
import * as groups from "./groups"
|
import * as groups from "./groups"
|
||||||
import * as accounts from "./accounts"
|
import { v4 as uuid } from "uuid"
|
||||||
|
|
||||||
const TENANT_ID = "default"
|
const TENANT_ID = "default"
|
||||||
const TENANT_1 = "tenant1"
|
const TENANT_1 = "tenant1"
|
||||||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||||
|
|
||||||
const pkg = {
|
const pkg = {
|
||||||
|
...structures,
|
||||||
|
uuid,
|
||||||
configs,
|
configs,
|
||||||
users,
|
users,
|
||||||
accounts,
|
|
||||||
TENANT_ID,
|
TENANT_ID,
|
||||||
TENANT_1,
|
TENANT_1,
|
||||||
CSRF_TOKEN,
|
CSRF_TOKEN,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { v4 as uuid } from "uuid"
|
||||||
export const newEmail = () => {
|
export const newEmail = () => {
|
||||||
return `${uuid()}@test.com`
|
return `${uuid()}@test.com`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const user = (userProps?: any): User => {
|
export const user = (userProps?: any): User => {
|
||||||
return {
|
return {
|
||||||
email: newEmail(),
|
email: newEmail(),
|
||||||
|
|
Loading…
Reference in New Issue