re-write, to use the ideas that Rory put in place, still WIP, un-tested but all implemented.
This commit is contained in:
parent
f7d58c889d
commit
7a69dcef78
|
@ -1 +1,4 @@
|
|||
module.exports = require("./src/db/utils")
|
||||
module.exports = {
|
||||
...require("./src/db/utils"),
|
||||
...require("./src/db/constants"),
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("./src/tenancy")
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue