mirror of https://github.com/Budibase/budibase.git
127 changed files with 2353 additions and 875 deletions
@ -1 +1,4 @@ |
|||
module.exports = require("./src/db/utils") |
|||
module.exports = { |
|||
...require("./src/db/utils"), |
|||
...require("./src/db/constants"), |
|||
} |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
exports.SEPARATOR = "_" |
|||
|
|||
exports.StaticDatabases = { |
|||
GLOBAL: { |
|||
name: "global-db", |
|||
docs: { |
|||
apiKeys: "apikeys", |
|||
}, |
|||
}, |
|||
// contains information about tenancy and so on
|
|||
PLATFORM_INFO: { |
|||
name: "global-info", |
|||
docs: { |
|||
tenants: "tenants", |
|||
}, |
|||
}, |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g |
|||
|
|||
exports.buildMatcherRegex = patterns => { |
|||
if (!patterns) { |
|||
return [] |
|||
} |
|||
return patterns.map(pattern => { |
|||
const isObj = typeof pattern === "object" && pattern.route |
|||
const method = isObj ? pattern.method : "GET" |
|||
let route = isObj ? pattern.route : pattern |
|||
|
|||
const matches = route.match(PARAM_REGEX) |
|||
if (matches) { |
|||
for (let match of matches) { |
|||
const pattern = "/.*" + (match.endsWith("/") ? "/" : "") |
|||
route = route.replace(match, pattern) |
|||
} |
|||
} |
|||
return { regex: new RegExp(route), method } |
|||
}) |
|||
} |
|||
|
|||
exports.matches = (ctx, options) => { |
|||
return options.find(({ regex, method }) => { |
|||
const urlMatch = regex.test(ctx.request.url) |
|||
const methodMatch = |
|||
method === "ALL" |
|||
? true |
|||
: ctx.request.method.toLowerCase() === method.toLowerCase() |
|||
|
|||
return urlMatch && methodMatch |
|||
}) |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
const { setTenantId } = require("../tenancy") |
|||
const ContextFactory = require("../tenancy/FunctionContext") |
|||
const { buildMatcherRegex, matches } = require("./matchers") |
|||
|
|||
module.exports = (allowQueryStringPatterns, noTenancyPatterns) => { |
|||
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) |
|||
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) |
|||
|
|||
return ContextFactory.getMiddleware(ctx => { |
|||
const allowNoTenant = !!matches(ctx, noTenancyOptions) |
|||
const allowQs = !!matches(ctx, allowQsOptions) |
|||
setTenantId(ctx, { allowQs, allowNoTenant }) |
|||
}) |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
const cls = require("cls-hooked") |
|||
const { newid } = require("../hashing") |
|||
|
|||
const REQUEST_ID_KEY = "requestId" |
|||
|
|||
class FunctionContext { |
|||
static getMiddleware(updateCtxFn = null) { |
|||
const namespace = this.createNamespace() |
|||
|
|||
return async function (ctx, next) { |
|||
await new Promise( |
|||
namespace.bind(function (resolve, reject) { |
|||
// store a contextual request ID that can be used anywhere (audit logs)
|
|||
namespace.set(REQUEST_ID_KEY, newid()) |
|||
namespace.bindEmitter(ctx.req) |
|||
namespace.bindEmitter(ctx.res) |
|||
|
|||
if (updateCtxFn) { |
|||
updateCtxFn(ctx) |
|||
} |
|||
next().then(resolve).catch(reject) |
|||
}) |
|||
) |
|||
} |
|||
} |
|||
|
|||
static run(callback) { |
|||
const namespace = this.createNamespace() |
|||
|
|||
return namespace.runAndReturn(callback) |
|||
} |
|||
|
|||
static setOnContext(key, value) { |
|||
const namespace = this.createNamespace() |
|||
namespace.set(key, value) |
|||
} |
|||
|
|||
static getContextStorage() { |
|||
if (this._namespace && this._namespace.active) { |
|||
let contextData = this._namespace.active |
|||
delete contextData.id |
|||
delete contextData._ns_name |
|||
return contextData |
|||
} |
|||
|
|||
return {} |
|||
} |
|||
|
|||
static getFromContext(key) { |
|||
const context = this.getContextStorage() |
|||
if (context) { |
|||
return context[key] |
|||
} else { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
static destroyNamespace() { |
|||
if (this._namespace) { |
|||
cls.destroyNamespace("session") |
|||
this._namespace = null |
|||
} |
|||
} |
|||
|
|||
static createNamespace() { |
|||
if (!this._namespace) { |
|||
this._namespace = cls.createNamespace("session") |
|||
} |
|||
return this._namespace |
|||
} |
|||
} |
|||
|
|||
module.exports = FunctionContext |
|||
@ -0,0 +1,81 @@ |
|||
const env = require("../environment") |
|||
const { Headers } = require("../../constants") |
|||
const cls = require("./FunctionContext") |
|||
|
|||
exports.DEFAULT_TENANT_ID = "default" |
|||
|
|||
exports.isDefaultTenant = () => { |
|||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID |
|||
} |
|||
|
|||
exports.isMultiTenant = () => { |
|||
return env.MULTI_TENANCY |
|||
} |
|||
|
|||
const TENANT_ID = "tenantId" |
|||
|
|||
// used for automations, API endpoints should always be in context already
|
|||
exports.doInTenant = (tenantId, task) => { |
|||
return cls.run(() => { |
|||
// set the tenant id
|
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
|
|||
// invoke the task
|
|||
const result = task() |
|||
|
|||
return result |
|||
}) |
|||
} |
|||
|
|||
exports.updateTenantId = tenantId => { |
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
} |
|||
|
|||
exports.setTenantId = ( |
|||
ctx, |
|||
opts = { allowQs: false, allowNoTenant: false } |
|||
) => { |
|||
let tenantId |
|||
// exit early if not multi-tenant
|
|||
if (!exports.isMultiTenant()) { |
|||
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID) |
|||
return |
|||
} |
|||
|
|||
const allowQs = opts && opts.allowQs |
|||
const allowNoTenant = opts && opts.allowNoTenant |
|||
const header = ctx.request.headers[Headers.TENANT_ID] |
|||
const user = ctx.user || {} |
|||
if (allowQs) { |
|||
const query = ctx.request.query || {} |
|||
tenantId = query.tenantId |
|||
} |
|||
// override query string (if allowed) by user, or header
|
|||
// URL params cannot be used in a middleware, as they are
|
|||
// processed later in the chain
|
|||
tenantId = user.tenantId || header || tenantId |
|||
|
|||
if (!tenantId && !allowNoTenant) { |
|||
ctx.throw(403, "Tenant id not set") |
|||
} |
|||
// check tenant ID just incase no tenant was allowed
|
|||
if (tenantId) { |
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
} |
|||
} |
|||
|
|||
exports.isTenantIdSet = () => { |
|||
const tenantId = cls.getFromContext(TENANT_ID) |
|||
return !!tenantId |
|||
} |
|||
|
|||
exports.getTenantId = () => { |
|||
if (!exports.isMultiTenant()) { |
|||
return exports.DEFAULT_TENANT_ID |
|||
} |
|||
const tenantId = cls.getFromContext(TENANT_ID) |
|||
if (!tenantId) { |
|||
throw Error("Tenant id not found") |
|||
} |
|||
return tenantId |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
module.exports = { |
|||
...require("./context"), |
|||
...require("./tenancy"), |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
const { getDB } = require("../db") |
|||
const { SEPARATOR, StaticDatabases } = require("../db/constants") |
|||
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context") |
|||
const env = require("../environment") |
|||
|
|||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants |
|||
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name |
|||
|
|||
exports.addTenantToUrl = url => { |
|||
const tenantId = getTenantId() |
|||
|
|||
if (isMultiTenant()) { |
|||
const char = url.indexOf("?") === -1 ? "?" : "&" |
|||
url += `${char}tenantId=${tenantId}` |
|||
} |
|||
|
|||
return url |
|||
} |
|||
|
|||
exports.doesTenantExist = async tenantId => { |
|||
const db = getDB(PLATFORM_INFO_DB) |
|||
let tenants |
|||
try { |
|||
tenants = await db.get(TENANT_DOC) |
|||
} catch (err) { |
|||
// if theres an error the doc doesn't exist, no tenants exist
|
|||
return false |
|||
} |
|||
return ( |
|||
tenants && |
|||
Array.isArray(tenants.tenantIds) && |
|||
tenants.tenantIds.indexOf(tenantId) !== -1 |
|||
) |
|||
} |
|||
|
|||
exports.tryAddTenant = async (tenantId, userId, email) => { |
|||
const db = getDB(PLATFORM_INFO_DB) |
|||
const getDoc = async id => { |
|||
if (!id) { |
|||
return null |
|||
} |
|||
try { |
|||
return await db.get(id) |
|||
} catch (err) { |
|||
return { _id: id } |
|||
} |
|||
} |
|||
let [tenants, userIdDoc, emailDoc] = await Promise.all([ |
|||
getDoc(TENANT_DOC), |
|||
getDoc(userId), |
|||
getDoc(email), |
|||
]) |
|||
if (!Array.isArray(tenants.tenantIds)) { |
|||
tenants = { |
|||
_id: TENANT_DOC, |
|||
tenantIds: [], |
|||
} |
|||
} |
|||
let promises = [] |
|||
if (userIdDoc) { |
|||
userIdDoc.tenantId = tenantId |
|||
promises.push(db.put(userIdDoc)) |
|||
} |
|||
if (emailDoc) { |
|||
emailDoc.tenantId = tenantId |
|||
promises.push(db.put(emailDoc)) |
|||
} |
|||
if (tenants.tenantIds.indexOf(tenantId) === -1) { |
|||
tenants.tenantIds.push(tenantId) |
|||
promises.push(db.put(tenants)) |
|||
} |
|||
await Promise.all(promises) |
|||
} |
|||
|
|||
exports.getGlobalDB = (tenantId = null) => { |
|||
// tenant ID can be set externally, for example user API where
|
|||
// new tenants are being created, this may be the case
|
|||
if (!tenantId) { |
|||
tenantId = getTenantId() |
|||
} |
|||
|
|||
let dbName |
|||
|
|||
if (tenantId === DEFAULT_TENANT_ID) { |
|||
dbName = StaticDatabases.GLOBAL.name |
|||
} else { |
|||
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` |
|||
} |
|||
|
|||
return getDB(dbName) |
|||
} |
|||
|
|||
exports.lookupTenantId = async userId => { |
|||
const db = getDB(StaticDatabases.PLATFORM_INFO.name) |
|||
let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null |
|||
try { |
|||
const doc = await db.get(userId) |
|||
if (doc && doc.tenantId) { |
|||
tenantId = doc.tenantId |
|||
} |
|||
} catch (err) { |
|||
// just return the default
|
|||
} |
|||
return tenantId |
|||
} |
|||
@ -0,0 +1 @@ |
|||
module.exports = require("./src/tenancy") |
|||
@ -1,4 +1,24 @@ |
|||
<script> |
|||
import { redirect } from "@roxi/routify" |
|||
$redirect("./login") |
|||
import { auth, admin } from "stores/portal" |
|||
import { onMount } from "svelte" |
|||
|
|||
$: tenantSet = $auth.tenantSet |
|||
$: multiTenancyEnabled = $admin.multiTenancy |
|||
|
|||
let loaded = false |
|||
|
|||
$: { |
|||
if (loaded && multiTenancyEnabled && !tenantSet) { |
|||
$redirect("./org") |
|||
} else if (loaded) { |
|||
$redirect("./login") |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
await admin.init() |
|||
await auth.checkQueryString() |
|||
loaded = true |
|||
}) |
|||
</script> |
|||
|
|||
@ -0,0 +1,73 @@ |
|||
<script> |
|||
import { Body, Button, Divider, Heading, Input, Layout } from "@budibase/bbui" |
|||
import { goto } from "@roxi/routify" |
|||
import { auth, admin } from "stores/portal" |
|||
import Logo from "assets/bb-emblem.svg" |
|||
import { get } from "svelte/store" |
|||
import { onMount } from "svelte" |
|||
|
|||
let tenantId = get(auth).tenantSet ? get(auth).tenantId : "" |
|||
$: multiTenancyEnabled = $admin.multiTenancy |
|||
|
|||
async function setOrg() { |
|||
if (tenantId == null || tenantId === "") { |
|||
tenantId = "default" |
|||
} |
|||
await auth.setOrg(tenantId) |
|||
// re-init now org selected |
|||
await admin.init() |
|||
$goto("../") |
|||
} |
|||
|
|||
function handleKeydown(evt) { |
|||
if (evt.key === "Enter") setOrg() |
|||
} |
|||
|
|||
onMount(async () => { |
|||
await auth.checkQueryString() |
|||
if (!multiTenancyEnabled) { |
|||
$goto("../") |
|||
} else { |
|||
admin.unload() |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<svelte:window on:keydown={handleKeydown} /> |
|||
<div class="login"> |
|||
<div class="main"> |
|||
<Layout> |
|||
<Layout noPadding justifyItems="center"> |
|||
<img alt="logo" src={Logo} /> |
|||
<Heading>Set Budibase organisation</Heading> |
|||
</Layout> |
|||
<Divider noGrid /> |
|||
<Layout gap="XS" noPadding> |
|||
<Body size="S" textAlign="center">Set organisation</Body> |
|||
<Input label="Organisation" bind:value={tenantId} /> |
|||
</Layout> |
|||
<Layout gap="XS" noPadding> |
|||
<Button cta on:click={setOrg}>Set organisation</Button> |
|||
</Layout> |
|||
</Layout> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.login { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.main { |
|||
width: 300px; |
|||
} |
|||
|
|||
img { |
|||
width: 48px; |
|||
} |
|||
</style> |
|||
@ -1,4 +1,11 @@ |
|||
<script> |
|||
import { redirect } from "@roxi/routify" |
|||
$redirect("./builder") |
|||
import { auth } from "../stores/portal" |
|||
import { onMount } from "svelte" |
|||
|
|||
auth.checkQueryString() |
|||
|
|||
onMount(() => { |
|||
$redirect(`./builder`) |
|||
}) |
|||
</script> |
|||
|
|||
@ -0,0 +1,8 @@ |
|||
#!/usr/bin/env node
|
|||
const updateDotEnv = require("update-dotenv") |
|||
|
|||
const arg = process.argv.slice(2)[0] |
|||
|
|||
updateDotEnv({ |
|||
MULTI_TENANCY: arg === "enable" ? "1" : "", |
|||
}).then(() => console.log("Updated server!")) |
|||
@ -1,38 +0,0 @@ |
|||
const CouchDB = require("./index") |
|||
const { StaticDatabases } = require("./utils") |
|||
const env = require("../environment") |
|||
|
|||
const SELF_HOST_ERR = "Unable to access builder DB/doc - not self hosted." |
|||
const BUILDER_DB = StaticDatabases.BUILDER |
|||
|
|||
/** |
|||
* This is the builder database, right now this is a single, static database |
|||
* that is present across the whole system and determines some core functionality |
|||
* for the builder (e.g. storage of API keys). This has been limited to self hosting |
|||
* as it doesn't make as much sense against the currently design Cloud system. |
|||
*/ |
|||
|
|||
exports.getBuilderMainDoc = async () => { |
|||
if (!env.SELF_HOSTED) { |
|||
throw SELF_HOST_ERR |
|||
} |
|||
const db = new CouchDB(BUILDER_DB.name) |
|||
try { |
|||
return await db.get(BUILDER_DB.baseDoc) |
|||
} catch (err) { |
|||
// doesn't exist yet, nothing to get
|
|||
return { |
|||
_id: BUILDER_DB.baseDoc, |
|||
} |
|||
} |
|||
} |
|||
|
|||
exports.setBuilderMainDoc = async doc => { |
|||
if (!env.SELF_HOSTED) { |
|||
throw SELF_HOST_ERR |
|||
} |
|||
// make sure to override the ID
|
|||
doc._id = BUILDER_DB.baseDoc |
|||
const db = new CouchDB(BUILDER_DB.name) |
|||
return db.put(doc) |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
#!/usr/bin/env node
|
|||
const updateDotEnv = require("update-dotenv") |
|||
|
|||
const arg = process.argv.slice(2)[0] |
|||
|
|||
updateDotEnv({ |
|||
MULTI_TENANCY: arg === "enable" ? "1" : "", |
|||
}).then(() => console.log("Updated worker!")) |
|||
@ -1,19 +1,16 @@ |
|||
const { sendEmail } = require("../../../utilities/email") |
|||
const CouchDB = require("../../../db") |
|||
const authPkg = require("@budibase/auth") |
|||
|
|||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name |
|||
const { getGlobalDB } = require("@budibase/auth/tenancy") |
|||
|
|||
exports.sendEmail = async ctx => { |
|||
const { groupId, email, userId, purpose, contents, from, subject } = |
|||
let { workspaceId, email, userId, purpose, contents, from, subject } = |
|||
ctx.request.body |
|||
let user |
|||
if (userId) { |
|||
const db = new CouchDB(GLOBAL_DB) |
|||
const db = getGlobalDB() |
|||
user = await db.get(userId) |
|||
} |
|||
const response = await sendEmail(email, purpose, { |
|||
groupId, |
|||
workspaceId, |
|||
user, |
|||
contents, |
|||
from, |
|||
@ -0,0 +1,7 @@ |
|||
const env = require("../../../environment") |
|||
|
|||
exports.fetch = async ctx => { |
|||
ctx.body = { |
|||
multiTenancy: !!env.MULTI_TENANCY, |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
const CouchDB = require("../../../db") |
|||
const { StaticDatabases } = require("@budibase/auth/db") |
|||
|
|||
exports.exists = async ctx => { |
|||
const tenantId = ctx.request.params |
|||
const db = new CouchDB(StaticDatabases.PLATFORM_INFO.name) |
|||
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
|
|||
} |
|||
ctx.body = { |
|||
exists, |
|||
} |
|||
} |
|||
|
|||
exports.fetch = async ctx => { |
|||
const db = new CouchDB(StaticDatabases.PLATFORM_INFO.name) |
|||
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
|
|||
} |
|||
ctx.body = tenants |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
const Router = require("@koa/router") |
|||
const authController = require("../../controllers/admin/auth") |
|||
const joiValidator = require("../../../middleware/joi-validator") |
|||
const Joi = require("joi") |
|||
|
|||
const router = Router() |
|||
|
|||
function buildAuthValidation() { |
|||
// prettier-ignore
|
|||
return joiValidator.body(Joi.object({ |
|||
username: Joi.string().required(), |
|||
password: Joi.string().required(), |
|||
}).required().unknown(false)) |
|||
} |
|||
|
|||
function buildResetValidation() { |
|||
// prettier-ignore
|
|||
return joiValidator.body(Joi.object({ |
|||
email: Joi.string().required(), |
|||
}).required().unknown(false)) |
|||
} |
|||
|
|||
function buildResetUpdateValidation() { |
|||
// prettier-ignore
|
|||
return joiValidator.body(Joi.object({ |
|||
resetCode: Joi.string().required(), |
|||
password: Joi.string().required(), |
|||
}).required().unknown(false)) |
|||
} |
|||
|
|||
router |
|||
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate) |
|||
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset) |
|||
.post( |
|||
"/api/admin/auth/reset/update", |
|||
buildResetUpdateValidation(), |
|||
authController.resetUpdate |
|||
) |
|||
.post("/api/admin/auth/logout", authController.logout) |
|||
.get("/api/admin/auth/google", authController.googlePreAuth) |
|||
.get("/api/admin/auth/google/callback", authController.googleAuth) |
|||
.get("/api/admin/auth/oidc/configs/:configId", authController.oidcPreAuth) |
|||
.get("/api/admin/auth/oidc/callback", authController.oidcAuth) |
|||
|
|||
module.exports = router |
|||
@ -1,11 +0,0 @@ |
|||
const Router = require("@koa/router") |
|||
const controller = require("../../controllers/admin/roles") |
|||
const adminOnly = require("../../../middleware/adminOnly") |
|||
|
|||
const router = Router() |
|||
|
|||
router |
|||
.get("/api/admin/roles", adminOnly, controller.fetch) |
|||
.get("/api/admin/roles/:appId", adminOnly, controller.find) |
|||
|
|||
module.exports = router |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue