1
0
Fork 0
mirror of synced 2024-06-14 00:14:39 +12:00

re-write, to use the ideas that Rory put in place, still WIP, un-tested but all implemented.

This commit is contained in:
mike12345567 2021-08-02 18:34:43 +01:00
parent f6d0db4c4b
commit 7743384f77
32 changed files with 461 additions and 258 deletions

View file

@ -1 +1,4 @@
module.exports = require("./src/db/utils")
module.exports = {
...require("./src/db/utils"),
...require("./src/db/constants"),
}

View file

@ -13,6 +13,7 @@
"@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.901.0",
"bcryptjs": "^2.4.3",
"cls-hooked": "^4.2.2",
"ioredis": "^4.27.1",
"jsonwebtoken": "^8.5.1",
"koa-passport": "^4.1.4",

View file

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

View file

@ -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",
},
},
}

View file

@ -3,29 +3,16 @@ const Replication = require("./Replication")
const { getDB } = require("./index")
const { DEFAULT_TENANT_ID } = require("../constants")
const env = require("../environment")
const { StaticDatabases, SEPARATOR } = require("./constants")
const { getTenantId } = require("../tenancy")
const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
exports.ViewNames = {
USER_BY_EMAIL: "by_email",
}
exports.StaticDatabases = {
GLOBAL: {
name: "global-db",
docs: {
apiKeys: "apikeys",
},
},
// contains information about tenancy and so on
PLATFORM_INFO: {
name: "global-info",
docs: {
tenants: "tenants",
},
},
}
exports.StaticDatabases = StaticDatabases
const PRE_APP = "app"
const PRE_DEV = "dev"
@ -74,45 +61,6 @@ function getDocParams(docType, docId = null, otherProps = {}) {
}
}
/**
* Gets the name of the global DB to connect to in a multi-tenancy system.
*/
exports.getGlobalDB = tenantId => {
// fallback for system pre multi-tenancy
let dbName = exports.StaticDatabases.GLOBAL.name
if (tenantId && tenantId !== DEFAULT_TENANT_ID) {
dbName = `${tenantId}${SEPARATOR}${dbName}`
}
if (env.MULTI_TENANCY && tenantId == null) {
throw "Cannot create global DB without tenantId"
}
return getDB(dbName)
}
/**
* Given a koa context this tries to extra what tenant is being accessed.
*/
exports.getTenantIdFromCtx = (ctx, opts = { includeQuery: false }) => {
if (!ctx) {
return null
}
const user = ctx.user || {}
const params = ctx.request.params || {}
let query = {}
if (opts && opts.includeQuery) {
query = ctx.request.query || {}
}
return user.tenantId || params.tenantId || query.tenantId
}
/**
* Given a koa context this tries to find the correct tenant Global DB.
*/
exports.getGlobalDBFromCtx = (ctx, opts) => {
const tenantId = exports.getTenantIdFromCtx(ctx, opts)
return exports.getGlobalDB(tenantId)
}
/**
* Generates a new workspace ID.
* @returns {string} The new workspace ID which the workspace doc can be stored under.
@ -216,7 +164,8 @@ exports.getDeployedAppID = appId => {
* different users/companies apps as there is no security around it - all apps are returned.
* @return {Promise<object[]>} returns the app information document stored in each app database.
*/
exports.getAllApps = async (CouchDB, { tenantId, dev, all } = {}) => {
exports.getAllApps = async (CouchDB, { dev, all } = {}) => {
let tenantId = getTenantId()
if (!env.MULTI_TENANCY && !tenantId) {
tenantId = DEFAULT_TENANT_ID
}

View file

@ -1,7 +1,8 @@
const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy
const { getGlobalDB, StaticDatabases } = require("./db/utils")
const { StaticDatabases } = require("./db/utils")
const { getGlobalDB } = require("./tenancy")
const {
jwt,
local,
@ -9,6 +10,7 @@ const {
google,
oidc,
auditLog,
tenancy,
} = require("./middleware")
const { setDB } = require("./db")
const userCache = require("./cache/user")
@ -20,7 +22,7 @@ passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser(async (user, done) => {
const db = getGlobalDB(user.tenantId)
const db = getGlobalDB()
try {
const user = await db.get(user._id)
@ -54,6 +56,7 @@ module.exports = {
google,
oidc,
jwt: require("jsonwebtoken"),
buildTenancyMiddleware: tenancy,
auditLog,
},
cache: {

View file

@ -2,27 +2,10 @@ const { Cookies, Headers } = require("../constants")
const { getCookie, clearCookie } = require("../utils")
const { getUser } = require("../cache/user")
const { getSession, updateSessionTTL } = require("../security/sessions")
const { buildMatcherRegex, matches } = require("./matchers")
const { isTenantIdSet, updateTenantId } = require("../tenancy")
const env = require("../environment")
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g
function buildNoAuthRegex(patterns) {
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 }
})
}
function finalise(
ctx,
{ authenticated, user, internal, version, publicEndpoint } = {}
@ -34,19 +17,14 @@ function finalise(
ctx.version = version
}
module.exports = (noAuthPatterns = [], opts) => {
const noAuthOptions = noAuthPatterns ? buildNoAuthRegex(noAuthPatterns) : []
module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
return async (ctx, next) => {
let publicEndpoint = false
const version = ctx.request.headers[Headers.API_VER]
// the path is not authenticated
const found = noAuthOptions.find(({ regex, method }) => {
return (
regex.test(ctx.request.url) &&
ctx.request.method.toLowerCase() === method.toLowerCase()
)
})
if (found != null) {
const found = matches(ctx, noAuthOptions)
if (found) {
publicEndpoint = true
}
try {
@ -64,7 +42,10 @@ module.exports = (noAuthPatterns = [], opts) => {
error = "No session found"
} else {
try {
user = await getUser(userId, session.tenantId)
if (session.tenantId && !isTenantIdSet()) {
updateTenantId(session.tenantId)
}
user = await getUser(userId)
delete user.password
authenticated = true
} catch (err) {

View file

@ -4,6 +4,7 @@ const google = require("./passport/google")
const oidc = require("./passport/oidc")
const authenticated = require("./authenticated")
const auditLog = require("./auditLog")
const tenancy = require("./tenancy")
module.exports = {
google,
@ -12,4 +13,5 @@ module.exports = {
local,
authenticated,
auditLog,
tenancy,
}

View file

@ -0,0 +1,30 @@
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g
exports.buildMatcherRegex = patterns => {
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
})
}

View file

@ -1,11 +1,12 @@
const jwt = require("jsonwebtoken")
const { UserStatus, DEFAULT_TENANT_ID } = require("../../constants")
const { UserStatus } = require("../../constants")
const { compare } = require("../../hashing")
const env = require("../../environment")
const { getGlobalUserByEmail } = require("../../utils")
const { authError } = require("./utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const { getTenantId } = require("../../tenancy")
const INVALID_ERR = "Invalid Credentials"
@ -24,11 +25,8 @@ exports.options = {
exports.authenticate = async function (ctx, email, password, done) {
if (!email) return authError(done, "Email Required")
if (!password) return authError(done, "Password Required")
const params = ctx.params || {}
// use the request to find the tenantId
let tenantId = params.tenantId || DEFAULT_TENANT_ID
const dbUser = await getGlobalUserByEmail(email, tenantId)
const dbUser = await getGlobalUserByEmail(email)
if (dbUser == null) {
return authError(done, "User not found")
}
@ -41,6 +39,7 @@ exports.authenticate = async function (ctx, email, password, done) {
// authenticate
if (await compare(password, dbUser.password)) {
const sessionId = newid()
const tenantId = getTenantId()
await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign(

View file

@ -1,10 +1,11 @@
const env = require("../../environment")
const jwt = require("jsonwebtoken")
const { generateGlobalUserID, getGlobalDB } = require("../../db/utils")
const { generateGlobalUserID } = require("../../db/utils")
const { authError } = require("./utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const { getGlobalUserByEmail, lookupTenantId } = require("../../utils")
const { getGlobalUserByEmail } = require("../../utils")
const { getGlobalDB, getTenantId } = require("../../tenancy")
/**
* Common authentication logic for third parties. e.g. OAuth, OIDC.
@ -26,8 +27,7 @@ exports.authenticateThirdParty = async function (
// use the third party id
const userId = generateGlobalUserID(thirdPartyUser.userId)
const tenantId = await lookupTenantId(userId)
const db = getGlobalDB(tenantId)
const db = getGlobalDB()
let dbUser
@ -47,7 +47,7 @@ exports.authenticateThirdParty = async function (
// fallback to loading by email
if (!dbUser) {
dbUser = await getGlobalUserByEmail(thirdPartyUser.email, tenantId)
dbUser = await getGlobalUserByEmail(thirdPartyUser.email)
}
// exit early if there is still no user and auto creation is disabled
@ -75,6 +75,7 @@ exports.authenticateThirdParty = async function (
// authenticate
const sessionId = newid()
const tenantId = getTenantId()
await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign(

View file

@ -0,0 +1,23 @@
const {
createTenancyContext,
setTenantId,
} = require("../tenancy")
const { buildMatcherRegex, matches } = require("./matchers")
module.exports = (allowQueryStringPatterns, noTenancyPatterns) => {
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
return (ctx, next) => {
// always run in context
return createTenancyContext().runAndReturn(() => {
if (matches(ctx, noTenancyOptions)) {
return next()
}
const allowQs = !!matches(ctx, allowQsOptions)
setTenantId(ctx, { allowQs })
return next()
})
}
}

View file

@ -0,0 +1,87 @@
const cls = require("cls-hooked")
const env = require("../environment")
const { Headers } = require("../../constants")
exports.DEFAULT_TENANT_ID = "default"
exports.isDefaultTenant = () => {
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
}
exports.isMultiTenant = () => {
return env.MULTI_TENANCY
}
// continuation local storage
const CONTEXT_NAME = "tenancy"
const TENANT_ID = "tenantId"
exports.createTenancyContext = () => {
return cls.createNamespace(CONTEXT_NAME)
}
const getTenancyContext = () => {
return cls.getNamespace(CONTEXT_NAME)
}
// used for automations, API endpoints should always be in context already
exports.doInTenant = (tenantId, task) => {
const context = getTenancyContext()
return getTenancyContext().runAndReturn(() => {
// set the tenant id
context.set(TENANT_ID, tenantId)
// invoke the task
const result = task()
// clear down the tenant id manually for extra safety
// this should also happen automatically when the call exits
context.set(TENANT_ID, null)
return result
})
}
exports.updateTenantId = tenantId => {
getTenancyContext().set(TENANT_ID, tenantId)
}
exports.setTenantId = (ctx, opts = { allowQs: false }) => {
let tenantId
// exit early if not multi-tenant
if (!exports.isMultiTenant()) {
getTenancyContext().set(TENANT_ID, this.DEFAULT_TENANT_ID)
return
}
const params = ctx.request.params || {}
const header = ctx.request.headers[Headers.TENANT_ID]
const user = ctx.request.user || {}
tenantId = user.tenantId || params.tenantId || header
if (opts.allowQs && !tenantId) {
const query = ctx.request.query || {}
tenantId = query.tenantId
}
if (!tenantId) {
ctx.throw(403, "Tenant id not set")
}
getTenancyContext().set(TENANT_ID, tenantId)
}
exports.isTenantIdSet = () => {
const tenantId = getTenancyContext().get(TENANT_ID)
return !!tenantId
}
exports.getTenantId = () => {
if (!exports.isMultiTenant()) {
return exports.DEFAULT_TENANT_ID
}
const tenantId = getTenancyContext().get(TENANT_ID)
if (!tenantId) {
throw Error("Tenant id not found")
}
return tenantId
}

View file

@ -0,0 +1,4 @@
module.exports = {
...require("./context"),
...require("./tenancy"),
}

View file

@ -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) {
const 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
}

View file

@ -2,15 +2,12 @@ const {
DocumentTypes,
SEPARATOR,
ViewNames,
StaticDatabases,
} = require("./db/utils")
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 { DEFAULT_TENANT_ID, Headers } = require("./constants")
const env = require("./environment")
const { Headers } = require("./constants")
const { getGlobalDB } = require("./tenancy")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -103,32 +100,17 @@ exports.isClient = ctx => {
return ctx.headers[Headers.TYPE] === "client"
}
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
}
/**
* 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, tenantId) => {
exports.getGlobalUserByEmail = async email => {
if (email == null) {
throw "Must supply an email address to view"
}
const db = getGlobalDB(tenantId)
const db = getGlobalDB()
try {
let users = (
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
@ -141,7 +123,7 @@ exports.getGlobalUserByEmail = async (email, tenantId) => {
} catch (err) {
if (err != null && err.name === "not_found") {
await createUserEmailView(db)
return exports.getGlobalUserByEmail(email, tenantId)
return exports.getGlobalUserByEmail(email)
} else {
throw err
}

1
packages/auth/tenancy.js Normal file
View file

@ -0,0 +1 @@
module.exports = require("./src/tenancy")

View file

@ -798,6 +798,13 @@ ast-types@0.9.6:
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
async-hook-jl@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
dependencies:
stack-chain "^1.3.7"
async@~2.1.4:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
@ -1144,6 +1151,15 @@ clone-buffer@1.0.0:
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
cls-hooked@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
dependencies:
async-hook-jl "^1.7.6"
emitter-listener "^1.0.1"
semver "^5.4.1"
cluster-key-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
@ -1444,6 +1460,13 @@ electron-to-chromium@^1.3.723:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082"
integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q==
emitter-listener@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
dependencies:
shimmer "^1.2.0"
emittery@^0.7.1:
version "0.7.2"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82"
@ -4035,7 +4058,7 @@ saxes@^5.0.1:
dependencies:
xmlchars "^2.2.0"
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -4096,6 +4119,11 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shimmer@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -4250,6 +4278,11 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
stack-chain@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
stack-utils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"

View file

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

View file

@ -1,5 +1,5 @@
const Router = require("@koa/router")
const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth
const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } = require("@budibase/auth").auth
const currentApp = require("../middleware/currentapp")
const compress = require("koa-compress")
const zlib = require("zlib")
@ -31,6 +31,7 @@ router
})
.use("/health", ctx => (ctx.status = 200))
.use("/version", ctx => (ctx.body = pkg.version))
.use(buildTenancyMiddleware())
.use(
buildAuthMiddleware(null, {
publicAllowed: true,

View file

@ -18,7 +18,7 @@ const { cleanup } = require("../../utilities/fileSystem")
const { Cookies, Headers } = require("@budibase/auth").constants
const { jwt } = require("@budibase/auth").auth
const auth = require("@budibase/auth")
const { getGlobalDB } = require("@budibase/auth/db")
const { getGlobalDB } = require("@budibase/auth/tenancy")
const { createASession } = require("@budibase/auth/sessions")
const { user: userCache } = require("@budibase/auth/cache")
const CouchDB = require("../../db")

View file

@ -3,9 +3,10 @@ const {
getGlobalIDFromUserMetadataID,
} = require("../db/utils")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { getDeployedAppID, getGlobalDBFromCtx } = require("@budibase/auth/db")
const { getDeployedAppID } = require("@budibase/auth/db")
const { getGlobalUserParams } = require("@budibase/auth/db")
const { user: userCache } = require("@budibase/auth/cache")
const { getGlobalDB } = require("@budibase/auth/tenancy")
exports.updateAppRole = (appId, user) => {
if (!user.roles) {
@ -37,13 +38,13 @@ exports.getCachedSelf = async (ctx, appId) => {
}
exports.getGlobalUser = async (ctx, appId, userId) => {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
return processUser(appId, user)
}
exports.getGlobalUsers = async (ctx, appId = null, users = null) => {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
let globalUsers
if (users) {
const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id))

View file

@ -8,13 +8,13 @@ const { setCookie, getCookie, clearCookie, getGlobalUserByEmail, hash } =
const { Cookies } = authPkg.constants
const { passport } = authPkg.auth
const { checkResetPasswordCode } = require("../../../utilities/redis")
const { getGlobalDB } = authPkg.db
const { getGlobalDB, getTenantId, isMultiTenant } = require("@budibase/auth/tenancy")
const env = require("../../../environment")
function googleCallbackUrl(tenantId = null) {
function googleCallbackUrl() {
let callbackUrl = `/api/global/auth`
if (tenantId) {
callbackUrl += `/${tenantId}`
if (isMultiTenant()) {
callbackUrl += `/${getTenantId()}`
}
callbackUrl += `/google/callback`
return callbackUrl
@ -57,8 +57,7 @@ exports.authenticate = async (ctx, next) => {
*/
exports.reset = async ctx => {
const { email } = ctx.request.body
const tenantId = ctx.params.tenantId
const configured = await isEmailConfigured(tenantId)
const configured = await isEmailConfigured()
if (!configured) {
ctx.throw(
400,
@ -66,10 +65,10 @@ exports.reset = async ctx => {
)
}
try {
const user = await getGlobalUserByEmail(email, tenantId)
const user = await getGlobalUserByEmail(email)
// only if user exists, don't error though if they don't
if (user) {
await sendEmail(tenantId, email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
user,
subject: "{{ company }} platform password reset",
})
@ -90,7 +89,7 @@ exports.resetUpdate = async ctx => {
const { resetCode, password } = ctx.request.body
try {
const userId = await checkResetPasswordCode(resetCode)
const db = getGlobalDB(ctx.params.tenantId)
const db = getGlobalDB()
const user = await db.get(userId)
user.password = await hash(password)
await db.put(user)
@ -112,9 +111,8 @@ exports.logout = async ctx => {
* On a successful login, you will be redirected to the googleAuth callback route.
*/
exports.googlePreAuth = async (ctx, next) => {
const tenantId = ctx.params ? ctx.params.tenantId : null
const db = getGlobalDB(tenantId)
let callbackUrl = googleCallbackUrl(tenantId)
const db = getGlobalDB()
let callbackUrl = googleCallbackUrl()
const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE,
@ -128,9 +126,8 @@ exports.googlePreAuth = async (ctx, next) => {
}
exports.googleAuth = async (ctx, next) => {
const tenantId = ctx.params ? ctx.params.tenantId : null
const db = getGlobalDB(tenantId)
const callbackUrl = googleCallbackUrl(tenantId)
const db = getGlobalDB()
const callbackUrl = googleCallbackUrl()
const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE,
@ -150,8 +147,7 @@ exports.googleAuth = async (ctx, next) => {
}
async function oidcStrategyFactory(ctx, configId) {
const tenantId = ctx.params ? ctx.params.tenantId : null
const db = getGlobalDB(ctx.params.tenantId)
const db = getGlobalDB()
const config = await authPkg.db.getScopedConfig(db, {
type: Configs.OIDC,
group: ctx.query.group,
@ -161,8 +157,8 @@ async function oidcStrategyFactory(ctx, configId) {
const protocol = env.NODE_ENV === "production" ? "https" : "http"
let callbackUrl = `${protocol}://${ctx.host}/api/global/auth`
if (tenantId) {
callbackUrl += `/${tenantId}`
if (isMultiTenant()) {
callbackUrl += `/${getTenantId()}`
}
callbackUrl += `/oidc/callback`

View file

@ -3,17 +3,16 @@ const {
getConfigParams,
getGlobalUserParams,
getScopedFullConfig,
getGlobalDBFromCtx,
getTenantIdFromCtx,
getAllApps,
} = require("@budibase/auth/db")
const { Configs } = require("../../../constants")
const email = require("../../../utilities/email")
const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore
const CouchDB = require("../../../db")
const { getGlobalDB } = require("@budibase/auth/tenancy")
exports.save = async function (ctx) {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
const { type, workspace, user, config } = ctx.request.body
// Config does not exist yet
@ -49,7 +48,7 @@ exports.save = async function (ctx) {
}
exports.fetch = async function (ctx) {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
const response = await db.allDocs(
getConfigParams(
{ type: ctx.params.type },
@ -66,7 +65,7 @@ exports.fetch = async function (ctx) {
* The hierarchy is type -> workspace -> user.
*/
exports.find = async function (ctx) {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
const { userId, workspaceId } = ctx.query
if (workspaceId && userId) {
@ -99,7 +98,7 @@ exports.find = async function (ctx) {
}
exports.publicOidc = async function (ctx) {
const db = getGlobalDBFromCtx(ctx, { includeQuery: true })
const db = getGlobalDB()
try {
// Find the config with the most granular scope based on context
const oidcConfig = await getScopedFullConfig(db, {
@ -121,7 +120,7 @@ exports.publicOidc = async function (ctx) {
}
exports.publicSettings = async function (ctx) {
const db = getGlobalDBFromCtx(ctx, { includeQuery: true })
const db = getGlobalDB()
try {
// Find the config with the most granular scope based on context
@ -186,7 +185,7 @@ exports.upload = async function (ctx) {
// add to configuration structure
// TODO: right now this only does a global level
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
let cfgStructure = await getScopedFullConfig(db, { type })
if (!cfgStructure) {
cfgStructure = {
@ -206,7 +205,7 @@ exports.upload = async function (ctx) {
}
exports.destroy = async function (ctx) {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
const { id, rev } = ctx.params
try {
@ -218,15 +217,13 @@ exports.destroy = async function (ctx) {
}
exports.configChecklist = async function (ctx) {
// include the query string only for a select few endpoints
const tenantId = getTenantIdFromCtx(ctx, { includeQuery: true })
const db = getGlobalDBFromCtx(ctx, { includeQuery: true })
const db = getGlobalDB()
try {
// TODO: Watch get started video
// Apps exist
const apps = await getAllApps(CouchDB, { tenantId })
const apps = await getAllApps(CouchDB)
// They have set up SMTP
const smtpConfig = await getScopedFullConfig(db, {

View file

@ -1,9 +1,8 @@
const { sendEmail } = require("../../../utilities/email")
const { getGlobalDBFromCtx } = require("@budibase/auth/db")
const { getGlobalDB } = require("@budibase/auth/tenancy")
exports.sendEmail = async ctx => {
let {
tenantId,
workspaceId,
email,
userId,
@ -14,13 +13,10 @@ exports.sendEmail = async ctx => {
} = ctx.request.body
let user
if (userId) {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
user = await db.get(userId)
}
if (!tenantId && ctx.user.tenantId) {
tenantId = ctx.user.tenantId
}
const response = await sendEmail(tenantId, email, purpose, {
const response = await sendEmail(email, purpose, {
workspaceId,
user,
contents,

View file

@ -1,13 +1,14 @@
const { generateTemplateID, getGlobalDBFromCtx } = require("@budibase/auth/db")
const { generateTemplateID } = require("@budibase/auth/db")
const {
TemplateMetadata,
TemplateBindings,
GLOBAL_OWNER,
} = require("../../../constants")
const { getTemplatesCtx } = require("../../../constants/templates")
const { getTemplates } = require("../../../constants/templates")
const { getGlobalDB } = require("@budibase/auth/tenancy")
exports.save = async ctx => {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
let template = ctx.request.body
if (!template.ownerId) {
template.ownerId = GLOBAL_OWNER
@ -45,29 +46,29 @@ exports.definitions = async ctx => {
}
exports.fetch = async ctx => {
ctx.body = await getTemplatesCtx(ctx)
ctx.body = await getTemplates()
}
exports.fetchByType = async ctx => {
ctx.body = await getTemplatesCtx(ctx, {
ctx.body = await getTemplates({
type: ctx.params.type,
})
}
exports.fetchByOwner = async ctx => {
ctx.body = await getTemplatesCtx(ctx, {
ctx.body = await getTemplates({
ownerId: ctx.params.ownerId,
})
}
exports.find = async ctx => {
ctx.body = await getTemplatesCtx(ctx, {
ctx.body = await getTemplates({
id: ctx.params.id,
})
}
exports.destroy = async ctx => {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
await db.remove(ctx.params.id, ctx.params.rev)
ctx.message = `Template ${ctx.params.id} deleted.`
ctx.status = 200

View file

@ -1,8 +1,7 @@
const {
generateGlobalUserID,
getGlobalUserParams,
getGlobalDB,
getGlobalDBFromCtx,
StaticDatabases,
} = require("@budibase/auth/db")
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
@ -14,6 +13,7 @@ const { user: userCache } = require("@budibase/auth/cache")
const { invalidateSessions } = require("@budibase/auth/sessions")
const CouchDB = require("../../../db")
const env = require("../../../environment")
const { getGlobalDB, getTenantId } = require("@budibase/auth/tenancy")
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
@ -73,8 +73,8 @@ async function doesTenantExist(tenantId) {
)
}
async function allUsers(ctx) {
const db = getGlobalDBFromCtx(ctx)
async function allUsers() {
const db = getGlobalDB()
const response = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,
@ -87,12 +87,13 @@ async function saveUser(user, tenantId) {
if (!tenantId) {
throw "No tenancy specified."
}
// specify the tenancy incase we're making a new admin user (public)
const db = getGlobalDB(tenantId)
let { email, password, _id } = user
// make sure another user isn't using the same email
let dbUser
if (email) {
dbUser = await getGlobalUserByEmail(email, tenantId)
dbUser = await getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
throw "Email address already in use."
}
@ -148,10 +149,8 @@ async function saveUser(user, tenantId) {
}
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)
ctx.body = await saveUser(ctx.request.body, getTenantId())
} catch (err) {
ctx.throw(err.status || 400, err)
}
@ -163,7 +162,7 @@ exports.adminUser = async ctx => {
ctx.throw(403, "Organisation already exists.")
}
const db = getGlobalDB(tenantId)
const db = getGlobalDB()
const response = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,
@ -197,7 +196,7 @@ exports.adminUser = async ctx => {
}
exports.destroy = async ctx => {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
const dbUser = await db.get(ctx.params.id)
await db.remove(dbUser._id, dbUser._rev)
await userCache.invalidateUser(dbUser._id)
@ -209,7 +208,7 @@ exports.destroy = async ctx => {
exports.removeAppRole = async ctx => {
const { appId } = ctx.params
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
const users = await allUsers(ctx)
const bulk = []
const cacheInvalidations = []
@ -239,7 +238,7 @@ exports.getSelf = async ctx => {
}
exports.updateSelf = async ctx => {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
const user = await db.get(ctx.user._id)
if (ctx.request.body.password) {
ctx.request.body.password = await hash(ctx.request.body.password)
@ -272,7 +271,7 @@ exports.fetch = async ctx => {
// called internally by app server user find
exports.find = async ctx => {
const db = getGlobalDBFromCtx(ctx)
const db = getGlobalDB()
let user
try {
user = await db.get(ctx.params.id)
@ -310,16 +309,14 @@ exports.tenantLookup = async ctx => {
exports.invite = async ctx => {
let { email, userInfo } = ctx.request.body
const tenantId = ctx.user.tenantId
const existing = await getGlobalUserByEmail(email, tenantId)
const existing = await getGlobalUserByEmail(email)
if (existing) {
ctx.throw(400, "Email address already in use.")
}
if (!userInfo) {
userInfo = {}
}
userInfo.tenantId = tenantId
await sendEmail(tenantId, email, EmailTemplatePurpose.INVITATION, {
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
subject: "{{ company }} platform invitation",
info: userInfo,
})
@ -333,17 +330,13 @@ exports.inviteAccept = async ctx => {
try {
// info is an extension of the user object that was stored by global
const { email, info } = await checkInviteCode(inviteCode)
// only pass through certain props for accepting
ctx.request.body = {
ctx.body = await saveUser({
firstName,
lastName,
password,
email,
...info,
}
ctx.user = {
tenantId: info.tenantId,
}
}, info.tenantId)
// this will flesh out the body response
await exports.save(ctx)
} catch (err) {

View file

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

View file

@ -2,7 +2,18 @@ const Router = require("@koa/router")
const compress = require("koa-compress")
const zlib = require("zlib")
const { routes } = require("./routes")
const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth
const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } = require("@budibase/auth").auth
const NO_TENANCY_ENDPOINTS = [
{
route: "/api/system",
method: "ALL",
},
{
route: "/api/global/users/self",
method: "GET",
}
]
const PUBLIC_ENDPOINTS = [
{
@ -53,6 +64,7 @@ router
})
)
.use("/health", ctx => (ctx.status = 200))
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
// for now no public access is allowed to worker (bar health check)
.use((ctx, next) => {

View file

@ -8,9 +8,8 @@ const {
const { join } = require("path")
const {
getTemplateParams,
getTenantIdFromCtx,
getGlobalDB,
} = require("@budibase/auth/db")
const { getGlobalDB } = require("@budibase/auth/tenancy")
exports.EmailTemplates = {
[EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile(
@ -52,13 +51,8 @@ exports.addBaseTemplates = (templates, type = null) => {
return templates
}
exports.getTemplatesCtx = async (ctx, opts = {}) => {
const tenantId = getTenantIdFromCtx(ctx)
return exports.getTemplates(tenantId, opts)
}
exports.getTemplates = async (tenantId, { ownerId, type, id } = {}) => {
const db = getGlobalDB(tenantId)
exports.getTemplates = async ({ ownerId, type, id } = {}) => {
const db = getGlobalDB()
const response = await db.allDocs(
getTemplateParams(ownerId || GLOBAL_OWNER, id, {
include_docs: true,
@ -75,10 +69,7 @@ exports.getTemplates = async (tenantId, { ownerId, type, id } = {}) => {
return exports.addBaseTemplates(templates, type)
}
exports.getTemplateByPurpose = async ({ tenantId, ctx }, type, purpose) => {
if (!tenantId && ctx) {
tenantId = getTenantIdFromCtx(ctx)
}
const templates = await exports.getTemplates(tenantId, { type })
exports.getTemplateByPurpose = async (type, purpose) => {
const templates = await exports.getTemplates({ type })
return templates.find(template => template.purpose === purpose)
}

View file

@ -1,10 +1,11 @@
const nodemailer = require("nodemailer")
const { getGlobalDB, getScopedConfig } = require("@budibase/auth/db")
const { getScopedConfig } = require("@budibase/auth/db")
const { EmailTemplatePurpose, TemplateTypes, Configs } = require("../constants")
const { getTemplateByPurpose } = require("../constants/templates")
const { getSettingsTemplateContext } = require("./templates")
const { processString } = require("@budibase/string-templates")
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
const { getGlobalDB } = require("@budibase/auth/tenancy")
const TEST_MODE = false
const TYPE = TemplateTypes.EMAIL
@ -60,7 +61,6 @@ async function getLinkCode(purpose, email, user, info = null) {
/**
* Builds an email using handlebars and the templates found in the system (default or otherwise).
* @param {string} tenantId the ID of the tenant which is sending the email.
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
* @param {string} email the address which it is being sent to for contextual purposes.
* @param {object} context the context which is being used for building the email (hbs context).
@ -69,7 +69,6 @@ async function getLinkCode(purpose, email, user, info = null) {
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
*/
async function buildEmail(
tenantId,
purpose,
email,
context,
@ -80,8 +79,8 @@ async function buildEmail(
throw `Unable to build an email of type ${purpose}`
}
let [base, body] = await Promise.all([
getTemplateByPurpose({ tenantId }, TYPE, EmailTemplatePurpose.BASE),
getTemplateByPurpose({ tenantId }, TYPE, purpose),
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
getTemplateByPurpose(TYPE, purpose),
])
if (!base || !body) {
throw "Unable to build email, missing base components"
@ -123,12 +122,12 @@ async function getSmtpConfiguration(db, workspaceId = null) {
* Checks if a SMTP config exists based on passed in parameters.
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
*/
exports.isEmailConfigured = async (tenantId, workspaceId = null) => {
exports.isEmailConfigured = async (workspaceId = null) => {
// when "testing" simply return true
if (TEST_MODE) {
return true
}
const db = getGlobalDB(tenantId)
const db = getGlobalDB()
const config = await getSmtpConfiguration(db, workspaceId)
return config != null
}
@ -136,7 +135,6 @@ exports.isEmailConfigured = async (tenantId, 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.
@ -149,12 +147,11 @@ exports.isEmailConfigured = async (tenantId, workspaceId = null) => {
* nodemailer response.
*/
exports.sendEmail = async (
tenantId,
email,
purpose,
{ workspaceId, user, from, contents, subject, info } = {}
) => {
const db = getGlobalDB(tenantId)
const db = getGlobalDB()
let config = (await getSmtpConfiguration(db, workspaceId)) || {}
if (Object.keys(config).length === 0 && !TEST_MODE) {
throw "Unable to find SMTP configuration."
@ -162,11 +159,11 @@ 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(tenantId, purpose, code)
const context = await getSettingsTemplateContext(purpose, code)
const message = {
from: from || config.from,
to: email,
html: await buildEmail(tenantId, purpose, email, context, {
html: await buildEmail(purpose, email, context, {
user,
contents,
}),

View file

@ -1,4 +1,4 @@
const { getScopedConfig, getGlobalDB } = require("@budibase/auth/db")
const { getScopedConfig } = require("@budibase/auth/db")
const {
Configs,
InternalTemplateBindings,
@ -7,20 +7,13 @@ const {
} = require("../constants")
const { checkSlashesInUrl } = require("./index")
const env = require("../environment")
const { getGlobalDB, addTenantToUrl } = require("@budibase/auth/tenancy")
const LOCAL_URL = `http://localhost:${env.CLUSTER_PORT || 10000}`
const BASE_COMPANY = "Budibase"
function addTenantToUrl(url, tenantId) {
if (env.MULTI_TENANCY) {
const char = url.indexOf("?") === -1 ? "?" : "&"
url += `${char}tenantId=${tenantId}`
}
return url
}
exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => {
const db = getGlobalDB(tenantId)
exports.getSettingsTemplateContext = async (purpose, code = null) => {
const db = getGlobalDB()
// TODO: use more granular settings in the future if required
let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {}
if (!settings || !settings.platformUrl) {
@ -35,7 +28,7 @@ exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => {
[InternalTemplateBindings.DOCS_URL]:
settings.docsUrl || "https://docs.budibase.com/",
[InternalTemplateBindings.LOGIN_URL]: checkSlashesInUrl(
addTenantToUrl(`${URL}/login`, tenantId)
addTenantToUrl(`${URL}/login`)
),
[InternalTemplateBindings.CURRENT_DATE]: new Date().toISOString(),
[InternalTemplateBindings.CURRENT_YEAR]: new Date().getFullYear(),
@ -45,15 +38,14 @@ exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => {
case EmailTemplatePurpose.PASSWORD_RECOVERY:
context[InternalTemplateBindings.RESET_CODE] = code
context[InternalTemplateBindings.RESET_URL] = checkSlashesInUrl(
addTenantToUrl(`${URL}/builder/auth/reset?code=${code}`, tenantId)
addTenantToUrl(`${URL}/builder/auth/reset?code=${code}`)
)
break
case EmailTemplatePurpose.INVITATION:
context[InternalTemplateBindings.INVITE_CODE] = code
context[InternalTemplateBindings.INVITE_URL] = checkSlashesInUrl(
addTenantToUrl(
`${URL}/builder/invite?code=${code}&tenantId=${tenantId}`,
tenantId
`${URL}/builder/invite?code=${code}`
)
)
break