First version of multi-tenancy, work still to be done.

This commit is contained in:
mike12345567 2021-07-15 17:57:02 +01:00
parent afd642c60d
commit 912659a8ad
32 changed files with 243 additions and 174 deletions

View File

@ -1,15 +1,18 @@
const { getDB } = require("../db")
const { StaticDatabases } = require("../db/utils")
const { getGlobalDB } = require("../db/utils")
const redis = require("../redis/authRedis")
const { lookupTenantId } = require("../utils")
const EXPIRY_SECONDS = 3600
exports.getUser = async userId => {
exports.getUser = async (userId, tenantId = null) => {
if (!tenantId) {
tenantId = await lookupTenantId({ userId })
}
const client = await redis.getUserClient()
// try cache
let user = await client.get(userId)
if (!user) {
user = await getDB(StaticDatabases.GLOBAL.name).get(userId)
user = await getGlobalDB(tenantId).get(userId)
client.store(userId, user, EXPIRY_SECONDS)
}
return user

View File

@ -1,5 +1,6 @@
const { newid } = require("../hashing")
const Replication = require("./Replication")
const { getDB } = require("./index")
const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
@ -18,6 +19,9 @@ exports.StaticDatabases = {
// contains information about tenancy and so on
PLATFORM_INFO: {
name: "global-info",
docs: {
tenants: "tenants",
},
},
}
@ -64,6 +68,25 @@ function getDocParams(docType, docId = null, otherProps = {}) {
}
}
/**
* Gets the name of the global DB to connect to in a multi-tenancy system.
*/
exports.getGlobalDB = tenantId => {
const globalName = exports.StaticDatabases.GLOBAL.name
// fallback for system pre multi-tenancy
if (!tenantId) {
return globalName
}
return getDB(`${tenantId}${SEPARATOR}${globalName}`)
}
/**
* Given a koa context this tries to find the correct tenant Global DB.
*/
exports.getGlobalDBFromCtx = ctx => {
return exports.getGlobalDB(ctx.user.tenantId)
}
/**
* Generates a new workspace ID.
* @returns {string} The new workspace ID which the workspace doc can be stored under.

View File

@ -1,5 +1,4 @@
const { DocumentTypes, ViewNames, StaticDatabases } = require("./utils")
const { getDB } = require("./index")
const { DocumentTypes, ViewNames } = require("./utils")
function DesignDoc() {
return {
@ -10,8 +9,7 @@ function DesignDoc() {
}
}
exports.createUserEmailView = async () => {
const db = getDB(StaticDatabases.GLOBAL.name)
exports.createUserEmailView = async db => {
let designDoc
try {
designDoc = await db.get("_design/database")

View File

@ -1,9 +1,9 @@
const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy
const { StaticDatabases } = require("./db/utils")
const { getGlobalDB } = require("./db/utils")
const { jwt, local, authenticated, google, auditLog } = require("./middleware")
const { setDB, getDB } = require("./db")
const { setDB } = require("./db")
const userCache = require("./cache/user")
// Strategies
@ -13,7 +13,7 @@ passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser(async (user, done) => {
const db = getDB(StaticDatabases.GLOBAL.name)
const db = getGlobalDB(user.tenantId)
try {
const user = await db.get(user._id)

View File

@ -56,7 +56,7 @@ module.exports = (noAuthPatterns = [], opts) => {
error = "No session found"
} else {
try {
user = await getUser(userId)
user = await getUser(userId, session.tenantId)
delete user.password
authenticated = true
} catch (err) {

View File

@ -1,23 +1,22 @@
const env = require("../../environment")
const jwt = require("jsonwebtoken")
const database = require("../../db")
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
const {
StaticDatabases,
generateGlobalUserID,
getGlobalDB,
ViewNames,
} = require("../../db/utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const { lookupTenantId } = require("../../utils")
async function authenticate(token, tokenSecret, profile, done) {
// Check the user exists in the instance DB by email
const db = database.getDB(StaticDatabases.GLOBAL.name)
const userId = generateGlobalUserID(profile.id)
const tenantId = await lookupTenantId({ userId })
const db = getGlobalDB(tenantId)
let dbUser
const userId = generateGlobalUserID(profile.id)
try {
// use the google profile id
dbUser = await db.get(userId)
@ -62,7 +61,7 @@ async function authenticate(token, tokenSecret, profile, done) {
// authenticate
const sessionId = newid()
await createASession(dbUser._id, sessionId)
await createASession(dbUser._id, { sessionId, tenantId: dbUser.tenantId })
dbUser.token = jwt.sign(
{

View File

@ -34,12 +34,14 @@ exports.authenticate = async function (email, password, done) {
// authenticate
if (await compare(password, dbUser.password)) {
const sessionId = newid()
await createASession(dbUser._id, sessionId)
const tenantId = dbUser.tenantId
await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign(
{
userId: dbUser._id,
sessionId,
tenantId,
},
env.JWT_SECRET
)

View File

@ -12,12 +12,13 @@ function makeSessionID(userId, sessionId) {
return `${userId}/${sessionId}`
}
exports.createASession = async (userId, sessionId) => {
exports.createASession = async (userId, session) => {
const client = await redis.getSessionClient()
const session = {
const sessionId = session.sessionId
session = {
createdAt: new Date().toISOString(),
lastAccessedAt: new Date().toISOString(),
sessionId,
...session,
userId,
}
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)

View File

@ -8,6 +8,7 @@ const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt")
const { createUserEmailView } = require("./db/views")
const { getDB } = require("./db")
const { getGlobalDB } = require("./db/utils")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -100,17 +101,31 @@ exports.isClient = ctx => {
return ctx.headers["x-budibase-type"] === "client"
}
exports.lookupTenantId = async ({ email, userId }) => {
const toQuery = email || userId
const db = getDB(StaticDatabases.PLATFORM_INFO.name)
const doc = await db.get(toQuery)
if (!doc || !doc.tenantId) {
throw "Unable to find tenant"
}
return doc.tenantId
}
/**
* Given an email address this will use a view to search through
* all the users to find one with this email address.
* @param {string} email the email to lookup the user by.
* @param {string|null} tenantId If tenant ID is known it can be specified
* @return {Promise<object|null>}
*/
exports.getGlobalUserByEmail = async email => {
exports.getGlobalUserByEmail = async (email, tenantId = null) => {
if (email == null) {
throw "Must supply an email address to view"
}
const db = getDB(StaticDatabases.GLOBAL.name)
if (!tenantId) {
tenantId = await exports.lookupTenantId({ email })
}
const db = getGlobalDB(tenantId)
try {
let users = (
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
@ -122,7 +137,7 @@ exports.getGlobalUserByEmail = async email => {
return users.length <= 1 ? users[0] : users
} catch (err) {
if (err != null && err.name === "not_found") {
await createUserEmailView()
await createUserEmailView(db)
return exports.getGlobalUserByEmail(email)
} else {
throw err

View File

@ -1,11 +1,10 @@
const CouchDB = require("../../db")
const { StaticDatabases } = require("@budibase/auth/db")
const { StaticDatabases, getGlobalDBFromCtx } = require("@budibase/auth/db")
const GLOBAL_DB = StaticDatabases.GLOBAL.name
const KEYS_DOC = StaticDatabases.GLOBAL.docs.apiKeys
async function getBuilderMainDoc() {
const db = new CouchDB(GLOBAL_DB)
async function getBuilderMainDoc(ctx) {
const db = getGlobalDBFromCtx(ctx)
try {
return await db.get(KEYS_DOC)
} catch (err) {
@ -16,17 +15,17 @@ async function getBuilderMainDoc() {
}
}
async function setBuilderMainDoc(doc) {
async function setBuilderMainDoc(ctx, doc) {
// make sure to override the ID
doc._id = KEYS_DOC
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
return db.put(doc)
}
exports.fetch = async function (ctx) {
try {
const mainDoc = await getBuilderMainDoc()
const mainDoc = await getBuilderMainDoc(ctx)
ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {}
} catch (err) {
/* istanbul ignore next */
@ -39,12 +38,12 @@ exports.update = async function (ctx) {
const value = ctx.request.body.value
try {
const mainDoc = await getBuilderMainDoc()
const mainDoc = await getBuilderMainDoc(ctx)
if (mainDoc.apiKeys == null) {
mainDoc.apiKeys = {}
}
mainDoc.apiKeys[key] = value
const resp = await setBuilderMainDoc(mainDoc)
const resp = await setBuilderMainDoc(ctx, mainDoc)
ctx.body = {
_id: resp.id,
_rev: resp.rev,

View File

@ -295,7 +295,7 @@ exports.delete = async function (ctx) {
await deleteApp(ctx.params.appId)
}
// make sure the app/role doesn't stick around after the app has been deleted
await removeAppFromUserRoles(ctx.params.appId)
await removeAppFromUserRoles(ctx, ctx.params.appId)
ctx.status = 200
ctx.body = result

View File

@ -22,7 +22,7 @@ exports.fetchSelf = async ctx => {
const userTable = await db.get(InternalTables.USER_METADATA)
const metadata = await db.get(userId)
// specifically needs to make sure is enriched
ctx.body = await outputProcessing(appId, userTable, {
ctx.body = await outputProcessing(ctx, userTable, {
...user,
...metadata,
})

View File

@ -159,6 +159,7 @@ exports.create = async function (ctx) {
automation._id = generateAutomationID()
automation.tenantId = ctx.user.tenantId
automation.type = "automation"
automation = cleanAutomationInputs(automation)
automation = await checkForWebhooks({

View File

@ -17,7 +17,7 @@ function removeGlobalProps(user) {
exports.fetchMetadata = async function (ctx) {
const database = new CouchDB(ctx.appId)
const global = await getGlobalUsers(ctx.appId)
const global = await getGlobalUsers(ctx, ctx.appId)
const metadata = (
await database.allDocs(
getUserMetadataParams(null, {

View File

@ -46,13 +46,13 @@ module.exports.definition = {
},
}
module.exports.run = async function ({ inputs }) {
module.exports.run = async function ({ inputs, tenantId }) {
let { to, from, subject, contents } = inputs
if (!contents) {
contents = "<h1>No content</h1>"
}
try {
let response = await sendSmtpEmail(to, from, subject, contents)
let response = await sendSmtpEmail(tenantId, to, from, subject, contents)
return {
success: true,
response,

View File

@ -22,6 +22,7 @@ class Orchestrator {
// step zero is never used as the template string is zero indexed for customer facing
this._context = { steps: [{}], trigger: triggerOutput }
this._automation = automation
this._tenantId = automation.tenantId
// create an emitter which has the chain count for this automation run in it, so it can block
// excessive chaining if required
this._emitter = new AutomationEmitter(this._chainCount + 1)
@ -57,6 +58,7 @@ class Orchestrator {
apiKey: automation.apiKey,
emitter: this._emitter,
context: this._context,
tenantId: this._tenantId,
})
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
break

View File

@ -60,7 +60,7 @@ async function getLinksForRows(appId, rows) {
)
}
async function getFullLinkedDocs(appId, links) {
async function getFullLinkedDocs(ctx, appId, links) {
// create DBs
const db = new CouchDB(appId)
const linkedRowIds = links.map(link => link.id)
@ -71,7 +71,7 @@ async function getFullLinkedDocs(appId, links) {
let [users, other] = partition(linked, linkRow =>
linkRow._id.startsWith(USER_METDATA_PREFIX)
)
const globalUsers = await getGlobalUsers(appId, users)
const globalUsers = await getGlobalUsers(ctx, appId, users)
users = users.map(user => {
const globalUser = globalUsers.find(
globalUser => globalUser && user._id.includes(globalUser._id)
@ -166,12 +166,13 @@ exports.attachLinkIDs = async (appId, rows) => {
/**
* Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row.
* This is required for formula fields, this may only be utilised internally (for now).
* @param {string} appId The app in which the tables/rows/links exist.
* @param {object} ctx The request which is looking for rows.
* @param {object} table The table from which the rows originated.
* @param {array<object>} rows The rows which are to be enriched.
* @return {Promise<*>} returns the rows with all of the enriched relationships on it.
*/
exports.attachFullLinkedDocs = async (appId, table, rows) => {
exports.attachFullLinkedDocs = async (ctx, table, rows) => {
const appId = ctx.appId
const linkedTableIds = getLinkedTableIDs(table)
if (linkedTableIds.length === 0) {
return rows
@ -182,7 +183,7 @@ exports.attachFullLinkedDocs = async (appId, table, rows) => {
const links = (await getLinksForRows(appId, rows)).filter(link =>
rows.some(row => row._id === link.thisId)
)
let linked = await getFullLinkedDocs(appId, links)
let linked = await getFullLinkedDocs(ctx, appId, links)
const linkedTables = []
for (let row of rows) {
for (let link of links.filter(link => link.thisId === row._id)) {

View File

@ -16,14 +16,14 @@ const supertest = require("supertest")
const { cleanup } = require("../../utilities/fileSystem")
const { Cookies } = require("@budibase/auth").constants
const { jwt } = require("@budibase/auth").auth
const { StaticDatabases } = require("@budibase/auth/db")
const { getGlobalDB } = require("@budibase/auth/db")
const { createASession } = require("@budibase/auth/sessions")
const { user: userCache } = require("@budibase/auth/cache")
const CouchDB = require("../../db")
const GLOBAL_USER_ID = "us_uuid1"
const EMAIL = "babs@babs.com"
const PASSWORD = "babs_password"
const TENANT_ID = "tenant1"
class TestConfiguration {
constructor(openServer = true) {
@ -65,7 +65,7 @@ class TestConfiguration {
}
async globalUser(id = GLOBAL_USER_ID, builder = true, roles) {
const db = new CouchDB(StaticDatabases.GLOBAL.name)
const db = getGlobalDB(TENANT_ID)
let existing
try {
existing = await db.get(id)
@ -76,6 +76,7 @@ class TestConfiguration {
_id: id,
...existing,
roles: roles || {},
tenantId: TENANT_ID,
}
await createASession(id, "sessionid")
if (builder) {

View File

@ -1,11 +1,9 @@
const CouchDB = require("../db")
const {
getMultiIDParams,
getGlobalIDFromUserMetadataID,
StaticDatabases,
} = require("../db/utils")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { getDeployedAppID } = require("@budibase/auth/db")
const { getDeployedAppID, getGlobalDBFromCtx } = require("@budibase/auth/db")
const { getGlobalUserParams } = require("@budibase/auth/db")
const { user: userCache } = require("@budibase/auth/cache")
@ -34,18 +32,18 @@ function processUser(appId, user) {
}
exports.getCachedSelf = async (ctx, appId) => {
const user = await userCache.getUser(ctx.user._id)
const user = await userCache.getUser(ctx.user._id, ctx.user.tenantId)
return processUser(appId, user)
}
exports.getGlobalUser = async (appId, userId) => {
const db = CouchDB(StaticDatabases.GLOBAL.name)
exports.getGlobalUser = async (ctx, appId, userId) => {
const db = getGlobalDBFromCtx(ctx)
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
return processUser(appId, user)
}
exports.getGlobalUsers = async (appId = null, users = null) => {
const db = CouchDB(StaticDatabases.GLOBAL.name)
exports.getGlobalUsers = async (ctx, appId = null, users = null) => {
const db = getGlobalDBFromCtx(ctx)
let globalUsers
if (users) {
const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id))

View File

@ -193,13 +193,14 @@ exports.inputProcessing = (user = {}, table, row) => {
/**
* This function enriches the input rows with anything they are supposed to contain, for example
* link records or attachment links.
* @param {string} appId the ID of the application for which rows are being enriched.
* @param {object} ctx the request which is looking for enriched rows.
* @param {object} table the table from which these rows came from originally, this is used to determine
* the schema of the rows and then enrich.
* @param {object[]} rows the rows which are to be enriched.
* @returns {object[]} the enriched rows will be returned.
*/
exports.outputProcessing = async (appId, table, rows) => {
exports.outputProcessing = async (ctx, table, rows) => {
const appId = ctx.appId
let wasArray = true
if (!(rows instanceof Array)) {
rows = [rows]

View File

@ -3,7 +3,7 @@ const { InternalTables } = require("../db/utils")
const { getGlobalUser } = require("../utilities/global")
exports.getFullUser = async (ctx, userId) => {
const global = await getGlobalUser(ctx.appId, userId)
const global = await getGlobalUser(ctx, ctx.appId, userId)
let metadata
try {
// this will throw an error if the db doesn't exist, or there is no appId

View File

@ -4,11 +4,11 @@ const { checkSlashesInUrl } = require("./index")
const { getDeployedAppID } = require("@budibase/auth/db")
const { updateAppRole, getGlobalUser } = require("./global")
function request(ctx, request, noApiKey) {
function request(ctx, request) {
if (!request.headers) {
request.headers = {}
}
if (!noApiKey) {
if (!ctx) {
request.headers["x-budibase-api-key"] = env.INTERNAL_API_KEY
}
if (request.body && Object.keys(request.body).length > 0) {
@ -28,12 +28,13 @@ function request(ctx, request, noApiKey) {
exports.request = request
exports.sendSmtpEmail = async (to, from, subject, contents) => {
exports.sendSmtpEmail = async (tenantId, to, from, subject, contents) => {
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`),
request(null, {
method: "POST",
body: {
tenantId,
email: to,
from,
contents,
@ -77,7 +78,7 @@ exports.getGlobalSelf = async (ctx, appId = null) => {
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
// we don't want to use API key when getting self
request(ctx, { method: "GET" }, true)
request(ctx, { method: "GET" })
)
if (response.status !== 200) {
ctx.throw(400, "Unable to get self globally.")
@ -97,7 +98,7 @@ exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => {
user = await exports.getGlobalSelf(ctx)
endpoint = `/api/admin/users/self`
} else {
user = await getGlobalUser(appId, userId)
user = await getGlobalUser(ctx, appId, userId)
body._id = userId
endpoint = `/api/admin/users`
}
@ -121,11 +122,11 @@ exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => {
return response.json()
}
exports.removeAppFromUserRoles = async appId => {
exports.removeAppFromUserRoles = async (ctx, appId) => {
const deployedAppId = getDeployedAppID(appId)
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/admin/roles/${deployedAppId}`),
request(null, {
request(ctx, {
method: "DELETE",
})
)

View File

@ -1,14 +1,12 @@
const authPkg = require("@budibase/auth")
const { google } = require("@budibase/auth/src/middleware")
const { Configs, EmailTemplatePurpose } = require("../../../constants")
const CouchDB = require("../../../db")
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
const { clearCookie, getGlobalUserByEmail, hash } = authPkg.utils
const { Cookies } = authPkg.constants
const { passport } = authPkg.auth
const { checkResetPasswordCode } = require("../../../utilities/redis")
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
const { getGlobalDB } = authPkg.db
async function authInternal(ctx, user, err = null) {
if (err) {
@ -46,7 +44,8 @@ exports.authenticate = async (ctx, next) => {
*/
exports.reset = async ctx => {
const { email } = ctx.request.body
const configured = await isEmailConfigured()
const tenantId = ctx.params.tenantId
const configured = await isEmailConfigured(tenantId)
if (!configured) {
ctx.throw(
400,
@ -54,10 +53,10 @@ exports.reset = async ctx => {
)
}
try {
const user = await getGlobalUserByEmail(email)
const user = await getGlobalUserByEmail(email, tenantId)
// only if user exists, don't error though if they don't
if (user) {
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
await sendEmail(tenantId, email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
user,
subject: "{{ company }} platform password reset",
})
@ -77,7 +76,7 @@ exports.resetUpdate = async ctx => {
const { resetCode, password } = ctx.request.body
try {
const userId = await checkResetPasswordCode(resetCode)
const db = new CouchDB(GLOBAL_DB)
const db = new getGlobalDB(ctx.params.tenantId)
const user = await db.get(userId)
user.password = await hash(password)
await db.put(user)
@ -99,7 +98,7 @@ exports.logout = async ctx => {
* On a successful login, you will be redirected to the googleAuth callback route.
*/
exports.googlePreAuth = async (ctx, next) => {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDB(ctx.params.tenantId)
const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE,
workspace: ctx.query.workspace,
@ -112,7 +111,7 @@ exports.googlePreAuth = async (ctx, next) => {
}
exports.googleAuth = async (ctx, next) => {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDB(ctx.params.tenantId)
const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE,

View File

@ -1,21 +1,18 @@
const CouchDB = require("../../../db")
const {
generateConfigID,
StaticDatabases,
getConfigParams,
getGlobalUserParams,
getScopedFullConfig,
} = require("@budibase/auth").db
getGlobalDBFromCtx,
getAllApps,
} = require("@budibase/auth/db")
const { Configs } = require("../../../constants")
const email = require("../../../utilities/email")
const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore
const APP_PREFIX = "app_"
const GLOBAL_DB = StaticDatabases.GLOBAL.name
exports.save = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const { type, workspace, user, config } = ctx.request.body
// Config does not exist yet
@ -51,7 +48,7 @@ exports.save = async function (ctx) {
}
exports.fetch = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const response = await db.allDocs(
getConfigParams(
{ type: ctx.params.type },
@ -68,7 +65,7 @@ exports.fetch = async function (ctx) {
* The hierarchy is type -> workspace -> user.
*/
exports.find = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const { userId, workspaceId } = ctx.query
if (workspaceId && userId) {
@ -101,7 +98,7 @@ exports.find = async function (ctx) {
}
exports.publicSettings = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
try {
// Find the config with the most granular scope based on context
const config = await getScopedFullConfig(db, {
@ -139,7 +136,7 @@ exports.upload = async function (ctx) {
// add to configuration structure
// TODO: right now this only does a global level
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
let cfgStructure = await getScopedFullConfig(db, { type })
if (!cfgStructure) {
cfgStructure = {
@ -159,7 +156,7 @@ exports.upload = async function (ctx) {
}
exports.destroy = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const { id, rev } = ctx.params
try {
@ -171,14 +168,13 @@ exports.destroy = async function (ctx) {
}
exports.configChecklist = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
try {
// TODO: Watch get started video
// Apps exist
let allDbs = await CouchDB.allDbs()
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
const apps = (await getAllApps({ CouchDB }))
// They have set up SMTP
const smtpConfig = await getScopedFullConfig(db, {
@ -199,7 +195,7 @@ exports.configChecklist = async function (ctx) {
const adminUser = users.rows.some(row => row.doc.admin)
ctx.body = {
apps: appDbNames.length,
apps: apps.length,
smtp: !!smtpConfig,
adminUser,
oauth: !!oauthConfig,

View File

@ -1,18 +1,18 @@
const { sendEmail } = require("../../../utilities/email")
const CouchDB = require("../../../db")
const authPkg = require("@budibase/auth")
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
const { getGlobalDBFromCtx } = require("@budibase/auth/db")
exports.sendEmail = async ctx => {
const { workspaceId, email, userId, purpose, contents, from, subject } =
let { tenantId, workspaceId, email, userId, purpose, contents, from, subject } =
ctx.request.body
let user
if (userId) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
user = await db.get(userId)
}
const response = await sendEmail(email, purpose, {
if (!tenantId && ctx.user.tenantId) {
tenantId = ctx.user.tenantId
}
const response = await sendEmail(tenantId, email, purpose, {
workspaceId,
user,
contents,

View File

@ -1,5 +1,4 @@
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db
const CouchDB = require("../../../db")
const { generateTemplateID, getGlobalDBFromCtx } = require("@budibase/auth/db")
const {
TemplateMetadata,
TemplateBindings,
@ -7,10 +6,8 @@ const {
} = require("../../../constants")
const { getTemplates } = require("../../../constants/templates")
const GLOBAL_DB = StaticDatabases.GLOBAL.name
exports.save = async ctx => {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
let template = ctx.request.body
if (!template.ownerId) {
template.ownerId = GLOBAL_OWNER
@ -42,29 +39,29 @@ exports.definitions = async ctx => {
}
exports.fetch = async ctx => {
ctx.body = await getTemplates()
ctx.body = await getTemplates(ctx)
}
exports.fetchByType = async ctx => {
ctx.body = await getTemplates({
ctx.body = await getTemplates(ctx, {
type: ctx.params.type,
})
}
exports.fetchByOwner = async ctx => {
ctx.body = await getTemplates({
ctx.body = await getTemplates(ctx, {
ownerId: ctx.params.ownerId,
})
}
exports.find = async ctx => {
ctx.body = await getTemplates({
ctx.body = await getTemplates(ctx, {
id: ctx.params.id,
})
}
exports.destroy = async ctx => {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
await db.remove(ctx.params.id, ctx.params.rev)
ctx.message = `Template ${ctx.params.id} deleted.`
ctx.status = 200

View File

@ -1,17 +1,43 @@
const CouchDB = require("../../../db")
const { generateGlobalUserID, getGlobalUserParams, StaticDatabases } =
require("@budibase/auth").db
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
const {
generateGlobalUserID,
getGlobalUserParams,
getGlobalDB,
getGlobalDBFromCtx,
StaticDatabases
} = require("@budibase/auth/db")
const { hash, getGlobalUserByEmail, newid } = require("@budibase/auth").utils
const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
const { checkInviteCode } = require("../../../utilities/redis")
const { sendEmail } = require("../../../utilities/email")
const { user: userCache } = require("@budibase/auth/cache")
const { invalidateSessions } = require("@budibase/auth/sessions")
const CouchDB = require("../../../db")
const GLOBAL_DB = StaticDatabases.GLOBAL.name
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
const tenantDocId = StaticDatabases.PLATFORM_INFO.docs.tenants
async function allUsers() {
const db = new CouchDB(GLOBAL_DB)
async function noTenantsExist() {
const db = new CouchDB(PLATFORM_INFO_DB)
const tenants = await db.get(tenantDocId)
return !tenants || !tenants.tenantIds || tenants.tenantIds.length === 0
}
async function tryAddTenant(tenantId) {
const db = new CouchDB(PLATFORM_INFO_DB)
let tenants = await db.get(tenantDocId)
if (!tenants || !Array.isArray(tenants.tenantIds)) {
tenants = {
tenantIds: [],
}
}
if (tenants.tenantIds.indexOf(tenantId) === -1) {
tenants.tenantIds.push(tenantId)
await db.put(tenants)
}
}
async function allUsers(ctx) {
const db = getGlobalDBFromCtx(ctx)
const response = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,
@ -20,16 +46,19 @@ async function allUsers() {
return response.rows.map(row => row.doc)
}
exports.save = async ctx => {
const db = new CouchDB(GLOBAL_DB)
const { email, password, _id } = ctx.request.body
async function saveUser(user, tenantId) {
if (!tenantId) {
throw "No tenancy specified."
}
const db = getGlobalDB(tenantId)
await tryAddTenant(tenantId)
const { email, password, _id } = user
// make sure another user isn't using the same email
let dbUser
if (email) {
dbUser = await getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
ctx.throw(400, "Email address already in use.")
throw "Email address already in use."
}
} else {
dbUser = await db.get(_id)
@ -42,14 +71,15 @@ exports.save = async ctx => {
} else if (dbUser) {
hashedPassword = dbUser.password
} else {
ctx.throw(400, "Password must be specified.")
throw "Password must be specified."
}
let user = {
user = {
...dbUser,
...ctx.request.body,
...user,
_id: _id || generateGlobalUserID(),
password: hashedPassword,
tenantId,
}
// make sure the roles object is always present
if (!user.roles) {
@ -65,34 +95,37 @@ exports.save = async ctx => {
...user,
})
await userCache.invalidateUser(response.id)
ctx.body = {
return {
_id: response.id,
_rev: response.rev,
email,
}
} catch (err) {
if (err.status === 409) {
ctx.throw(400, "User exists already")
throw "User exists already"
} else {
ctx.throw(err.status, err)
throw err
}
}
}
exports.adminUser = async ctx => {
const db = new CouchDB(GLOBAL_DB)
const response = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,
})
)
exports.save = async ctx => {
// this always stores the user into the requesting users tenancy
const tenantId = ctx.user.tenantId
try {
ctx.body = await saveUser(ctx.request.body, tenantId)
} catch (err) {
ctx.throw(err.status || 400, err)
}
}
if (response.rows.some(row => row.doc.admin)) {
exports.adminUser = async ctx => {
if (!await noTenantsExist()) {
ctx.throw(403, "You cannot initialise once an admin user has been created.")
}
const { email, password } = ctx.request.body
ctx.request.body = {
const user = {
email: email,
password: password,
roles: {},
@ -103,11 +136,15 @@ exports.adminUser = async ctx => {
global: true,
},
}
await exports.save(ctx)
try {
ctx.body = await saveUser(user, newid())
} catch (err) {
ctx.throw(err.status || 400, err)
}
}
exports.destroy = async ctx => {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const dbUser = await db.get(ctx.params.id)
await db.remove(dbUser._id, dbUser._rev)
await userCache.invalidateUser(dbUser._id)
@ -119,7 +156,7 @@ exports.destroy = async ctx => {
exports.removeAppRole = async ctx => {
const { appId } = ctx.params
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const users = await allUsers()
const bulk = []
const cacheInvalidations = []
@ -149,7 +186,7 @@ exports.getSelf = async ctx => {
}
exports.updateSelf = async ctx => {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const user = await db.get(ctx.user._id)
if (ctx.request.body.password) {
ctx.request.body.password = await hash(ctx.request.body.password)
@ -170,7 +207,7 @@ exports.updateSelf = async ctx => {
// called internally by app server user fetch
exports.fetch = async ctx => {
const users = await allUsers()
const users = await allUsers(ctx)
// user hashed password shouldn't ever be returned
for (let user of users) {
if (user) {
@ -182,7 +219,7 @@ exports.fetch = async ctx => {
// called internally by app server user find
exports.find = async ctx => {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
let user
try {
user = await db.get(ctx.params.id)
@ -198,11 +235,12 @@ exports.find = async ctx => {
exports.invite = async ctx => {
const { email, userInfo } = ctx.request.body
const existing = await getGlobalUserByEmail(email)
const tenantId = ctx.user.tenantId
const existing = await getGlobalUserByEmail(email, tenantId)
if (existing) {
ctx.throw(400, "Email address already in use.")
}
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
await sendEmail(tenantId, email, EmailTemplatePurpose.INVITATION, {
subject: "{{ company }} platform invitation",
info: userInfo,
})

View File

@ -1,11 +1,8 @@
const CouchDB = require("../../../db")
const { getWorkspaceParams, generateWorkspaceID, StaticDatabases } =
require("@budibase/auth").db
const GLOBAL_DB = StaticDatabases.GLOBAL.name
const { getWorkspaceParams, generateWorkspaceID, getGlobalDBFromCtx } =
require("@budibase/auth/db")
exports.save = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const workspaceDoc = ctx.request.body
// workspace does not exist yet
@ -25,7 +22,7 @@ exports.save = async function (ctx) {
}
exports.fetch = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const response = await db.allDocs(
getWorkspaceParams(undefined, {
include_docs: true,
@ -35,7 +32,7 @@ exports.fetch = async function (ctx) {
}
exports.find = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
try {
ctx.body = await db.get(ctx.params.id)
} catch (err) {
@ -44,7 +41,7 @@ exports.find = async function (ctx) {
}
exports.destroy = async function (ctx) {
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDBFromCtx(ctx)
const { id, rev } = ctx.params
try {

View File

@ -30,14 +30,14 @@ function buildResetUpdateValidation() {
router
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate)
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset)
.post("/api/admin/auth/:tenantId/reset", buildResetValidation(), authController.reset)
.post(
"/api/admin/auth/reset/update",
"/api/admin/auth/:tenantId/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/:tenantId/google", authController.googlePreAuth)
.get("/api/admin/auth/:tenantId/google/callback", authController.googleAuth)
module.exports = router

View File

@ -6,8 +6,7 @@ const {
GLOBAL_OWNER,
} = require("../index")
const { join } = require("path")
const CouchDB = require("../../db")
const { getTemplateParams, StaticDatabases } = require("@budibase/auth").db
const { getTemplateParams, getGlobalDBFromCtx } = require("@budibase/auth/db")
exports.EmailTemplates = {
[EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile(
@ -49,8 +48,8 @@ exports.addBaseTemplates = (templates, type = null) => {
return templates
}
exports.getTemplates = async ({ ownerId, type, id } = {}) => {
const db = new CouchDB(StaticDatabases.GLOBAL.name)
exports.getTemplates = async (ctx, { ownerId, type, id } = {}) => {
const db = getGlobalDBFromCtx(ctx)
const response = await db.allDocs(
getTemplateParams(ownerId || GLOBAL_OWNER, id, {
include_docs: true,
@ -67,7 +66,7 @@ exports.getTemplates = async ({ ownerId, type, id } = {}) => {
return exports.addBaseTemplates(templates, type)
}
exports.getTemplateByPurpose = async (type, purpose) => {
const templates = await exports.getTemplates({ type })
exports.getTemplateByPurpose = async (ctx, type, purpose) => {
const templates = await exports.getTemplates(ctx, { type })
return templates.find(template => template.purpose === purpose)
}

View File

@ -1,6 +1,5 @@
const nodemailer = require("nodemailer")
const CouchDB = require("../db")
const { StaticDatabases, getScopedConfig } = require("@budibase/auth").db
const { getGlobalDB, getScopedConfig } = require("@budibase/auth/db")
const { EmailTemplatePurpose, TemplateTypes, Configs } = require("../constants")
const { getTemplateByPurpose } = require("../constants/templates")
const { getSettingsTemplateContext } = require("./templates")
@ -8,7 +7,6 @@ const { processString } = require("@budibase/string-templates")
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
const TEST_MODE = false
const GLOBAL_DB = StaticDatabases.GLOBAL.name
const TYPE = TemplateTypes.EMAIL
const FULL_EMAIL_PURPOSES = [
@ -116,15 +114,14 @@ async function getSmtpConfiguration(db, workspaceId = null) {
/**
* Checks if a SMTP config exists based on passed in parameters.
* @param workspaceId
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
*/
exports.isEmailConfigured = async (workspaceId = null) => {
exports.isEmailConfigured = async (tenantId, workspaceId = null) => {
// when "testing" simply return true
if (TEST_MODE) {
return true
}
const db = new CouchDB(GLOBAL_DB)
const db = getGlobalDB(tenantId)
const config = await getSmtpConfiguration(db, workspaceId)
return config != null
}
@ -132,6 +129,7 @@ exports.isEmailConfigured = async (workspaceId = null) => {
/**
* Given an email address and an email purpose this will retrieve the SMTP configuration and
* send an email using it.
* @param {string} tenantId The tenant which is sending them email.
* @param {string} email The email address to send to.
* @param {string} purpose The purpose of the email being sent (e.g. reset password).
* @param {string|undefined} workspaceId If finer grain controls being used then this will lookup config for workspace.
@ -144,11 +142,12 @@ exports.isEmailConfigured = async (workspaceId = null) => {
* nodemailer response.
*/
exports.sendEmail = async (
tenantId,
email,
purpose,
{ workspaceId, user, from, contents, subject, info } = {}
) => {
const db = new CouchDB(GLOBAL_DB)
const db = new getGlobalDB(tenantId)
let config = (await getSmtpConfiguration(db, workspaceId)) || {}
if (Object.keys(config).length === 0 && !TEST_MODE) {
throw "Unable to find SMTP configuration."
@ -156,7 +155,7 @@ exports.sendEmail = async (
const transport = createSMTPTransport(config)
// if there is a link code needed this will retrieve it
const code = await getLinkCode(purpose, email, user, info)
const context = await getSettingsTemplateContext(purpose, code)
const context = await getSettingsTemplateContext(tenantId, purpose, code)
const message = {
from: from || config.from,
to: email,

View File

@ -1,5 +1,4 @@
const CouchDB = require("../db")
const { getScopedConfig, StaticDatabases } = require("@budibase/auth").db
const { getScopedConfig, getGlobalDB } = require("@budibase/auth/db")
const {
Configs,
InternalTemplateBindings,
@ -12,8 +11,8 @@ const env = require("../environment")
const LOCAL_URL = `http://localhost:${env.CLUSTER_PORT || 10000}`
const BASE_COMPANY = "Budibase"
exports.getSettingsTemplateContext = async (purpose, code = null) => {
const db = new CouchDB(StaticDatabases.GLOBAL.name)
exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => {
const db = new getGlobalDB(tenantId)
// TODO: use more granular settings in the future if required
let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {}
if (!settings || !settings.platformUrl) {