Revert "Merge pull request #2253 from Budibase/revert-2076-feature/multi-tenants"

This reverts commit 4834b765be, reversing
changes made to 2456e69483.
This commit is contained in:
mike12345567 2021-08-05 09:59:08 +01:00
parent dafe110659
commit 46ea3f3e9e
127 changed files with 2359 additions and 881 deletions

View File

@ -26,10 +26,18 @@ static_resources:
cluster: couchdb-service cluster: couchdb-service
prefix_rewrite: "/" prefix_rewrite: "/"
- match: { prefix: "/api/system/" }
route:
cluster: worker-dev
- match: { prefix: "/api/admin/" } - match: { prefix: "/api/admin/" }
route: route:
cluster: worker-dev cluster: worker-dev
- match: { prefix: "/api/global/" }
route:
cluster: worker-dev
- match: { prefix: "/api/" } - match: { prefix: "/api/" }
route: route:
cluster: server-dev cluster: server-dev

View File

@ -37,11 +37,19 @@ static_resources:
route: route:
cluster: app-service cluster: app-service
# special case for worker admin API # special cases for worker admin (deprecated), global and system API
- match: { prefix: "/api/global/" }
route:
cluster: worker-service
- match: { prefix: "/api/admin/" } - match: { prefix: "/api/admin/" }
route: route:
cluster: worker-service cluster: worker-service
- match: { prefix: "/api/system/" }
route:
cluster: worker-service
- match: { path: "/" } - match: { path: "/" }
route: route:
cluster: app-service cluster: app-service

View File

@ -43,6 +43,8 @@
"test:e2e": "lerna run cy:test", "test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci", "test:e2e:ci": "lerna run cy:ci",
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -", "build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -",
"build:docker:develop": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -" "build:docker:develop": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"multi:enable": "lerna run multi:enable",
"multi:disable": "lerna run multi:disable"
} }
} }

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", "@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.901.0", "aws-sdk": "^2.901.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cls-hooked": "^4.2.2",
"ioredis": "^4.27.1", "ioredis": "^4.27.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa-passport": "^4.1.4", "koa-passport": "^4.1.4",

View File

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

View File

@ -14,13 +14,14 @@ exports.Headers = {
API_VER: "x-budibase-api-version", API_VER: "x-budibase-api-version",
APP_ID: "x-budibase-app-id", APP_ID: "x-budibase-app-id",
TYPE: "x-budibase-type", TYPE: "x-budibase-type",
TENANT_ID: "x-budibase-tenant-id",
} }
exports.GlobalRoles = { exports.GlobalRoles = {
OWNER: "owner", OWNER: "owner",
ADMIN: "admin", ADMIN: "admin",
BUILDER: "builder", BUILDER: "builder",
GROUP_MANAGER: "group_manager", WORKSPACE_MANAGER: "workspace_manager",
} }
exports.Configs = { exports.Configs = {
@ -31,3 +32,5 @@ exports.Configs = {
OIDC: "oidc", OIDC: "oidc",
OIDC_LOGOS: "logos_oidc", OIDC_LOGOS: "logos_oidc",
} }
exports.DEFAULT_TENANT_ID = "default"

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

@ -1,36 +1,36 @@
const { newid } = require("../hashing") const { newid } = require("../hashing")
const Replication = require("./Replication") const Replication = require("./Replication")
const { DEFAULT_TENANT_ID } = require("../constants")
const env = require("../environment")
const { StaticDatabases, SEPARATOR } = require("./constants")
const { getTenantId } = require("../tenancy")
const UNICODE_MAX = "\ufff0" const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
exports.ViewNames = { exports.ViewNames = {
USER_BY_EMAIL: "by_email", USER_BY_EMAIL: "by_email",
} }
exports.StaticDatabases = { exports.StaticDatabases = StaticDatabases
GLOBAL: {
name: "global-db", const PRE_APP = "app"
}, const PRE_DEV = "dev"
DEPLOYMENTS: {
name: "deployments",
},
}
const DocumentTypes = { const DocumentTypes = {
USER: "us", USER: "us",
GROUP: "group", WORKSPACE: "workspace",
CONFIG: "config", CONFIG: "config",
TEMPLATE: "template", TEMPLATE: "template",
APP: "app", APP: PRE_APP,
APP_DEV: "app_dev", DEV: PRE_DEV,
APP_METADATA: "app_metadata", APP_DEV: `${PRE_APP}${SEPARATOR}${PRE_DEV}`,
APP_METADATA: `${PRE_APP}${SEPARATOR}metadata`,
ROLE: "role", ROLE: "role",
} }
exports.DocumentTypes = DocumentTypes exports.DocumentTypes = DocumentTypes
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
exports.SEPARATOR = SEPARATOR exports.SEPARATOR = SEPARATOR
function isDevApp(app) { function isDevApp(app) {
@ -61,21 +61,21 @@ function getDocParams(docType, docId = null, otherProps = {}) {
} }
/** /**
* Generates a new group ID. * Generates a new workspace ID.
* @returns {string} The new group ID which the group doc can be stored under. * @returns {string} The new workspace ID which the workspace doc can be stored under.
*/ */
exports.generateGroupID = () => { exports.generateWorkspaceID = () => {
return `${DocumentTypes.GROUP}${SEPARATOR}${newid()}` return `${DocumentTypes.WORKSPACE}${SEPARATOR}${newid()}`
} }
/** /**
* Gets parameters for retrieving groups. * Gets parameters for retrieving workspaces.
*/ */
exports.getGroupParams = (id = "", otherProps = {}) => { exports.getWorkspaceParams = (id = "", otherProps = {}) => {
return { return {
...otherProps, ...otherProps,
startkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}`, startkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}`,
endkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}${UNICODE_MAX}`, endkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`,
} }
} }
@ -103,14 +103,14 @@ exports.getGlobalUserParams = (globalId, otherProps = {}) => {
/** /**
* Generates a template ID. * Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a group level. * @param ownerId The owner/user of the template, this could be global or a workspace level.
*/ */
exports.generateTemplateID = ownerId => { exports.generateTemplateID = ownerId => {
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}` return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
} }
/** /**
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a group level. * Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
*/ */
exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => { exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
if (!templateId) { if (!templateId) {
@ -163,11 +163,26 @@ exports.getDeployedAppID = appId => {
* different users/companies apps as there is no security around it - all apps are returned. * 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. * @return {Promise<object[]>} returns the app information document stored in each app database.
*/ */
exports.getAllApps = async ({ CouchDB, dev, all } = {}) => { exports.getAllApps = async (CouchDB, { dev, all } = {}) => {
let tenantId = getTenantId()
if (!env.MULTI_TENANCY && !tenantId) {
tenantId = DEFAULT_TENANT_ID
}
let allDbs = await CouchDB.allDbs() let allDbs = await CouchDB.allDbs()
const appDbNames = allDbs.filter(dbName => const appDbNames = allDbs.filter(dbName => {
dbName.startsWith(exports.APP_PREFIX) const split = dbName.split(SEPARATOR)
) // it is an app, check the tenantId
if (split[0] === DocumentTypes.APP) {
const noTenantId = split.length === 2 || split[1] === DocumentTypes.DEV
// tenantId is always right before the UUID
const possibleTenantId = split[split.length - 2]
return (
(tenantId === DEFAULT_TENANT_ID && noTenantId) ||
possibleTenantId === tenantId
)
}
return false
})
const appPromises = appDbNames.map(db => const appPromises = appDbNames.map(db =>
// skip setup otherwise databases could be re-created // skip setup otherwise databases could be re-created
new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA) new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA)
@ -214,8 +229,8 @@ exports.dbExists = async (CouchDB, dbName) => {
* Generates a new configuration ID. * Generates a new configuration ID.
* @returns {string} The new configuration ID which the config doc can be stored under. * @returns {string} The new configuration ID which the config doc can be stored under.
*/ */
const generateConfigID = ({ type, group, user }) => { const generateConfigID = ({ type, workspace, user }) => {
const scope = [type, group, user].filter(Boolean).join(SEPARATOR) const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}` return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
} }
@ -223,8 +238,8 @@ const generateConfigID = ({ type, group, user }) => {
/** /**
* Gets parameters for retrieving configurations. * Gets parameters for retrieving configurations.
*/ */
const getConfigParams = ({ type, group, user }, otherProps = {}) => { const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
const scope = [type, group, user].filter(Boolean).join(SEPARATOR) const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
return { return {
...otherProps, ...otherProps,
@ -234,15 +249,15 @@ const getConfigParams = ({ type, group, user }, otherProps = {}) => {
} }
/** /**
* Returns the most granular configuration document from the DB based on the type, group and userID passed. * Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
* @param {Object} db - db instance to query * @param {Object} db - db instance to query
* @param {Object} scopes - the type, group and userID scopes of the configuration. * @param {Object} scopes - the type, workspace and userID scopes of the configuration.
* @returns The most granular configuration document based on the scope. * @returns The most granular configuration document based on the scope.
*/ */
const getScopedFullConfig = async function (db, { type, user, group }) { const getScopedFullConfig = async function (db, { type, user, workspace }) {
const response = await db.allDocs( const response = await db.allDocs(
getConfigParams( getConfigParams(
{ type, user, group }, { type, user, workspace },
{ {
include_docs: true, include_docs: true,
} }
@ -252,14 +267,14 @@ const getScopedFullConfig = async function (db, { type, user, group }) {
function determineScore(row) { function determineScore(row) {
const config = row.doc const config = row.doc
// Config is specific to a user and a group // Config is specific to a user and a workspace
if (config._id.includes(generateConfigID({ type, user, group }))) { if (config._id.includes(generateConfigID({ type, user, workspace }))) {
return 4 return 4
} else if (config._id.includes(generateConfigID({ type, user }))) { } else if (config._id.includes(generateConfigID({ type, user }))) {
// Config is specific to a user only // Config is specific to a user only
return 3 return 3
} else if (config._id.includes(generateConfigID({ type, group }))) { } else if (config._id.includes(generateConfigID({ type, workspace }))) {
// Config is specific to a group only // Config is specific to a workspace only
return 2 return 2
} else if (config._id.includes(generateConfigID({ type }))) { } else if (config._id.includes(generateConfigID({ type }))) {
// Config is specific to a type only // Config is specific to a type only

View File

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

View File

@ -16,6 +16,7 @@ module.exports = {
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
MINIO_URL: process.env.MINIO_URL, MINIO_URL: process.env.MINIO_URL,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
MULTI_TENANCY: process.env.MULTI_TENANCY,
isTest, isTest,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value

View File

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

View File

@ -2,46 +2,34 @@ const { Cookies, Headers } = require("../constants")
const { getCookie, clearCookie } = require("../utils") const { getCookie, clearCookie } = require("../utils")
const { getUser } = require("../cache/user") const { getUser } = require("../cache/user")
const { getSession, updateSessionTTL } = require("../security/sessions") const { getSession, updateSessionTTL } = require("../security/sessions")
const { buildMatcherRegex, matches } = require("./matchers")
const env = require("../environment") const env = require("../environment")
const PARAM_REGEX = /\/:(.*?)\//g function finalise(
ctx,
function buildNoAuthRegex(patterns) { { authenticated, user, internal, version, publicEndpoint } = {}
return patterns.map(pattern => { ) {
const isObj = typeof pattern === "object" && pattern.route ctx.publicEndpoint = publicEndpoint || false
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) {
route = route.replace(match, "/.*/")
}
}
return { regex: new RegExp(route), method }
})
}
function finalise(ctx, { authenticated, user, internal, version } = {}) {
ctx.isAuthenticated = authenticated || false ctx.isAuthenticated = authenticated || false
ctx.user = user ctx.user = user
ctx.internal = internal || false ctx.internal = internal || false
ctx.version = version ctx.version = version
} }
module.exports = (noAuthPatterns = [], opts) => { /**
const noAuthOptions = noAuthPatterns ? buildNoAuthRegex(noAuthPatterns) : [] * This middleware is tenancy aware, so that it does not depend on other middlewares being used.
* The tenancy modules should not be used here and it should be assumed that the tenancy context
* has not yet been populated.
*/
module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
return async (ctx, next) => { return async (ctx, next) => {
let publicEndpoint = false
const version = ctx.request.headers[Headers.API_VER] const version = ctx.request.headers[Headers.API_VER]
// the path is not authenticated // the path is not authenticated
const found = noAuthOptions.find(({ regex, method }) => { const found = matches(ctx, noAuthOptions)
return ( if (found) {
regex.test(ctx.request.url) && publicEndpoint = true
ctx.request.method.toLowerCase() === method.toLowerCase()
)
})
if (found != null) {
return next()
} }
try { try {
// check the actual user is authenticated first // check the actual user is authenticated first
@ -58,7 +46,7 @@ module.exports = (noAuthPatterns = [], opts) => {
error = "No session found" error = "No session found"
} else { } else {
try { try {
user = await getUser(userId) user = await getUser(userId, session.tenantId)
delete user.password delete user.password
authenticated = true authenticated = true
} catch (err) { } catch (err) {
@ -75,22 +63,26 @@ module.exports = (noAuthPatterns = [], opts) => {
} }
} }
const apiKey = ctx.request.headers[Headers.API_KEY] const apiKey = ctx.request.headers[Headers.API_KEY]
const tenantId = ctx.request.headers[Headers.TENANT_ID]
// this is an internal request, no user made it // this is an internal request, no user made it
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) { if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) {
authenticated = true authenticated = true
internal = true internal = true
} }
if (!user && tenantId) {
user = { tenantId }
}
// be explicit // be explicit
if (authenticated !== true) { if (authenticated !== true) {
authenticated = false authenticated = false
} }
// isAuthenticated is a function, so use a variable to be able to check authed state // isAuthenticated is a function, so use a variable to be able to check authed state
finalise(ctx, { authenticated, user, internal, version }) finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
return next() return next()
} catch (err) { } catch (err) {
// allow configuring for public access // allow configuring for public access
if (opts && opts.publicAllowed) { if ((opts && opts.publicAllowed) || publicEndpoint) {
finalise(ctx, { authenticated: false, version }) finalise(ctx, { authenticated: false, version, publicEndpoint })
} else { } else {
ctx.throw(err.status || 403, err) ctx.throw(err.status || 403, err)
} }

View File

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

View File

@ -0,0 +1,33 @@
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g
exports.buildMatcherRegex = patterns => {
if (!patterns) {
return []
}
return patterns.map(pattern => {
const isObj = typeof pattern === "object" && pattern.route
const method = isObj ? pattern.method : "GET"
let route = isObj ? pattern.route : pattern
const matches = route.match(PARAM_REGEX)
if (matches) {
for (let match of matches) {
const pattern = "/.*" + (match.endsWith("/") ? "/" : "")
route = route.replace(match, pattern)
}
}
return { regex: new RegExp(route), method }
})
}
exports.matches = (ctx, options) => {
return options.find(({ regex, method }) => {
const urlMatch = regex.test(ctx.request.url)
const methodMatch =
method === "ALL"
? true
: ctx.request.method.toLowerCase() === method.toLowerCase()
return urlMatch && methodMatch
})
}

View File

@ -27,13 +27,13 @@ async function authenticate(accessToken, refreshToken, profile, done) {
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
* @returns Dynamically configured Passport Google Strategy * @returns Dynamically configured Passport Google Strategy
*/ */
exports.strategyFactory = async function (config) { exports.strategyFactory = async function (config, callbackUrl) {
try { try {
const { clientID, clientSecret, callbackURL } = config const { clientID, clientSecret } = config
if (!clientID || !clientSecret || !callbackURL) { if (!clientID || !clientSecret) {
throw new Error( throw new Error(
"Configuration invalid. Must contain google clientID, clientSecret and callbackURL" "Configuration invalid. Must contain google clientID and clientSecret"
) )
} }
@ -41,7 +41,7 @@ exports.strategyFactory = async function (config) {
{ {
clientID: config.clientID, clientID: config.clientID,
clientSecret: config.clientSecret, clientSecret: config.clientSecret,
callbackURL: config.callbackURL, callbackURL: callbackUrl,
}, },
authenticate authenticate
) )

View File

@ -6,19 +6,23 @@ const { getGlobalUserByEmail } = require("../../utils")
const { authError } = require("./utils") const { authError } = require("./utils")
const { newid } = require("../../hashing") const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions") const { createASession } = require("../../security/sessions")
const { getTenantId } = require("../../tenancy")
const INVALID_ERR = "Invalid Credentials" const INVALID_ERR = "Invalid Credentials"
exports.options = {} exports.options = {
passReqToCallback: true,
}
/** /**
* Passport Local Authentication Middleware. * Passport Local Authentication Middleware.
* @param {*} email - username to login with * @param {*} ctx the request structure
* @param {*} password - plain text password to log in with * @param {*} email username to login with
* @param {*} done - callback from passport to return user information and errors * @param {*} password plain text password to log in with
* @param {*} done callback from passport to return user information and errors
* @returns The authenticated user, or errors if they occur * @returns The authenticated user, or errors if they occur
*/ */
exports.authenticate = async function (email, password, done) { exports.authenticate = async function (ctx, email, password, done) {
if (!email) return authError(done, "Email Required") if (!email) return authError(done, "Email Required")
if (!password) return authError(done, "Password Required") if (!password) return authError(done, "Password Required")
@ -35,12 +39,14 @@ exports.authenticate = async function (email, password, done) {
// authenticate // authenticate
if (await compare(password, dbUser.password)) { if (await compare(password, dbUser.password)) {
const sessionId = newid() const sessionId = newid()
await createASession(dbUser._id, sessionId) const tenantId = getTenantId()
await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign( dbUser.token = jwt.sign(
{ {
userId: dbUser._id, userId: dbUser._id,
sessionId, sessionId,
tenantId,
}, },
env.JWT_SECRET env.JWT_SECRET
) )

View File

@ -2,8 +2,9 @@
const { data } = require("./utilities/mock-data") const { data } = require("./utilities/mock-data")
const TENANT_ID = "default"
const googleConfig = { const googleConfig = {
callbackURL: "http://somecallbackurl",
clientID: data.clientID, clientID: data.clientID,
clientSecret: data.clientSecret, clientSecret: data.clientSecret,
} }
@ -26,13 +27,14 @@ describe("google", () => {
it("should create successfully create a google strategy", async () => { it("should create successfully create a google strategy", async () => {
const google = require("../google") const google = require("../google")
await google.strategyFactory(googleConfig) const callbackUrl = `/api/global/auth/${TENANT_ID}/google/callback`
await google.strategyFactory(googleConfig, callbackUrl)
const expectedOptions = { const expectedOptions = {
clientID: googleConfig.clientID, clientID: googleConfig.clientID,
clientSecret: googleConfig.clientSecret, clientSecret: googleConfig.clientSecret,
callbackURL: googleConfig.callbackURL, callbackURL: callbackUrl,
} }
expect(mockStrategy).toHaveBeenCalledWith( expect(mockStrategy).toHaveBeenCalledWith(

View File

@ -1,11 +1,11 @@
const env = require("../../environment") const env = require("../../environment")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const database = require("../../db") const { generateGlobalUserID } = require("../../db/utils")
const { StaticDatabases, generateGlobalUserID } = require("../../db/utils")
const { authError } = require("./utils") const { authError } = require("./utils")
const { newid } = require("../../hashing") const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions") const { createASession } = require("../../security/sessions")
const { getGlobalUserByEmail } = require("../../utils") const { getGlobalUserByEmail } = require("../../utils")
const { getGlobalDB, getTenantId } = require("../../tenancy")
/** /**
* Common authentication logic for third parties. e.g. OAuth, OIDC. * Common authentication logic for third parties. e.g. OAuth, OIDC.
@ -15,19 +15,21 @@ exports.authenticateThirdParty = async function (
requireLocalAccount = true, requireLocalAccount = true,
done done
) { ) {
if (!thirdPartyUser.provider) if (!thirdPartyUser.provider) {
return authError(done, "third party user provider required") return authError(done, "third party user provider required")
if (!thirdPartyUser.userId) }
if (!thirdPartyUser.userId) {
return authError(done, "third party user id required") return authError(done, "third party user id required")
if (!thirdPartyUser.email) }
if (!thirdPartyUser.email) {
return authError(done, "third party user email required") return authError(done, "third party user email required")
}
const db = database.getDB(StaticDatabases.GLOBAL.name)
let dbUser
// use the third party id // use the third party id
const userId = generateGlobalUserID(thirdPartyUser.userId) const userId = generateGlobalUserID(thirdPartyUser.userId)
const db = getGlobalDB()
let dbUser
// try to load by id // try to load by id
try { try {
@ -73,7 +75,8 @@ exports.authenticateThirdParty = async function (
// authenticate // authenticate
const sessionId = newid() const sessionId = newid()
await createASession(dbUser._id, sessionId) const tenantId = getTenantId()
await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign( dbUser.token = jwt.sign(
{ {

View File

@ -0,0 +1,14 @@
const { setTenantId } = require("../tenancy")
const ContextFactory = require("../tenancy/FunctionContext")
const { buildMatcherRegex, matches } = require("./matchers")
module.exports = (allowQueryStringPatterns, noTenancyPatterns) => {
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
return ContextFactory.getMiddleware(ctx => {
const allowNoTenant = !!matches(ctx, noTenancyOptions)
const allowQs = !!matches(ctx, allowQsOptions)
setTenantId(ctx, { allowQs, allowNoTenant })
})
}

View File

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

View File

@ -0,0 +1,73 @@
const cls = require("cls-hooked")
const { newid } = require("../hashing")
const REQUEST_ID_KEY = "requestId"
class FunctionContext {
static getMiddleware(updateCtxFn = null) {
const namespace = this.createNamespace()
return async function (ctx, next) {
await new Promise(
namespace.bind(function (resolve, reject) {
// store a contextual request ID that can be used anywhere (audit logs)
namespace.set(REQUEST_ID_KEY, newid())
namespace.bindEmitter(ctx.req)
namespace.bindEmitter(ctx.res)
if (updateCtxFn) {
updateCtxFn(ctx)
}
next().then(resolve).catch(reject)
})
)
}
}
static run(callback) {
const namespace = this.createNamespace()
return namespace.runAndReturn(callback)
}
static setOnContext(key, value) {
const namespace = this.createNamespace()
namespace.set(key, value)
}
static getContextStorage() {
if (this._namespace && this._namespace.active) {
let contextData = this._namespace.active
delete contextData.id
delete contextData._ns_name
return contextData
}
return {}
}
static getFromContext(key) {
const context = this.getContextStorage()
if (context) {
return context[key]
} else {
return null
}
}
static destroyNamespace() {
if (this._namespace) {
cls.destroyNamespace("session")
this._namespace = null
}
}
static createNamespace() {
if (!this._namespace) {
this._namespace = cls.createNamespace("session")
}
return this._namespace
}
}
module.exports = FunctionContext

View File

@ -0,0 +1,81 @@
const env = require("../environment")
const { Headers } = require("../../constants")
const cls = require("./FunctionContext")
exports.DEFAULT_TENANT_ID = "default"
exports.isDefaultTenant = () => {
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
}
exports.isMultiTenant = () => {
return env.MULTI_TENANCY
}
const TENANT_ID = "tenantId"
// used for automations, API endpoints should always be in context already
exports.doInTenant = (tenantId, task) => {
return cls.run(() => {
// set the tenant id
cls.setOnContext(TENANT_ID, tenantId)
// invoke the task
const result = task()
return result
})
}
exports.updateTenantId = tenantId => {
cls.setOnContext(TENANT_ID, tenantId)
}
exports.setTenantId = (
ctx,
opts = { allowQs: false, allowNoTenant: false }
) => {
let tenantId
// exit early if not multi-tenant
if (!exports.isMultiTenant()) {
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID)
return
}
const allowQs = opts && opts.allowQs
const allowNoTenant = opts && opts.allowNoTenant
const header = ctx.request.headers[Headers.TENANT_ID]
const user = ctx.user || {}
if (allowQs) {
const query = ctx.request.query || {}
tenantId = query.tenantId
}
// override query string (if allowed) by user, or header
// URL params cannot be used in a middleware, as they are
// processed later in the chain
tenantId = user.tenantId || header || tenantId
if (!tenantId && !allowNoTenant) {
ctx.throw(403, "Tenant id not set")
}
// check tenant ID just incase no tenant was allowed
if (tenantId) {
cls.setOnContext(TENANT_ID, tenantId)
}
}
exports.isTenantIdSet = () => {
const tenantId = cls.getFromContext(TENANT_ID)
return !!tenantId
}
exports.getTenantId = () => {
if (!exports.isMultiTenant()) {
return exports.DEFAULT_TENANT_ID
}
const tenantId = cls.getFromContext(TENANT_ID)
if (!tenantId) {
throw Error("Tenant id not found")
}
return tenantId
}

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) {
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

@ -1,14 +1,9 @@
const { const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils")
DocumentTypes,
SEPARATOR,
ViewNames,
StaticDatabases,
} = require("./db/utils")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt") const { options } = require("./middleware/passport/jwt")
const { createUserEmailView } = require("./db/views") const { createUserEmailView } = require("./db/views")
const { getDB } = require("./db")
const { Headers } = require("./constants") const { Headers } = require("./constants")
const { getGlobalDB } = require("./tenancy")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -111,7 +106,7 @@ exports.getGlobalUserByEmail = async email => {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
const db = getDB(StaticDatabases.GLOBAL.name) const db = getGlobalDB()
try { try {
let users = ( let users = (
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
@ -123,7 +118,7 @@ exports.getGlobalUserByEmail = async email => {
return users.length <= 1 ? users[0] : users return users.length <= 1 ? users[0] : users
} catch (err) { } catch (err) {
if (err != null && err.name === "not_found") { if (err != null && err.name === "not_found") {
await createUserEmailView() await createUserEmailView(db)
return exports.getGlobalUserByEmail(email) return exports.getGlobalUserByEmail(email)
} else { } else {
throw err 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" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= 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: async@~2.1.4:
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" 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" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= 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: cluster-key-slot@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" 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" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082"
integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q== 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: emittery@^0.7.1:
version "0.7.2" version "0.7.2"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82"
@ -4035,7 +4058,7 @@ saxes@^5.0.1:
dependencies: dependencies:
xmlchars "^2.2.0" 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" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -4096,6 +4119,11 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== 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: signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" 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" safer-buffer "^2.0.2"
tweetnacl "~0.14.0" 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: stack-utils@^2.0.2:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"

View File

@ -4,7 +4,10 @@
import { onMount } from "svelte" import { onMount } from "svelte"
let loaded = false let loaded = false
$: multiTenancyEnabled = $admin.multiTenancy
$: hasAdminUser = !!$admin?.checklist?.adminUser $: hasAdminUser = !!$admin?.checklist?.adminUser
$: tenantSet = $auth.tenantSet
onMount(async () => { onMount(async () => {
await admin.init() await admin.init()
@ -12,9 +15,14 @@
loaded = true loaded = true
}) })
// Force creation of an admin user if one doesn't exist
$: { $: {
if (loaded && !hasAdminUser) { const apiReady = $admin.loaded && $auth.loaded
// if tenant is not set go to it
if (loaded && apiReady && multiTenancyEnabled && !tenantSet) {
$redirect("./auth/org")
}
// Force creation of an admin user if one doesn't exist
else if (loaded && apiReady && !hasAdminUser) {
$redirect("./admin") $redirect("./admin")
} }
} }
@ -29,7 +37,7 @@
!$isActive("./invite") !$isActive("./invite")
) { ) {
const returnUrl = encodeURIComponent(window.location.pathname) const returnUrl = encodeURIComponent(window.location.pathname)
$redirect("./auth/login?", { returnUrl }) $redirect("./auth?", { returnUrl })
} else if ($auth?.user?.forceResetPassword) { } else if ($auth?.user?.forceResetPassword) {
$redirect("./auth/reset") $redirect("./auth/reset")
} }

View File

@ -6,20 +6,25 @@
Layout, Layout,
Input, Input,
Body, Body,
ActionButton,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import api from "builderStore/api" import api from "builderStore/api"
import { admin } from "stores/portal" import { admin, auth } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte" import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
let adminUser = {} let adminUser = {}
let error let error
$: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy
async function save() { async function save() {
try { try {
adminUser.tenantId = tenantId
// Save the admin user // Save the admin user
const response = await api.post(`/api/admin/users/init`, adminUser) const response = await api.post(`/api/global/users/init`, adminUser)
const json = await response.json() const json = await response.json()
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(json.message) throw new Error(json.message)
@ -47,9 +52,22 @@
<Input label="Email" bind:value={adminUser.email} /> <Input label="Email" bind:value={adminUser.email} />
<PasswordRepeatInput bind:password={adminUser.password} bind:error /> <PasswordRepeatInput bind:password={adminUser.password} bind:error />
</Layout> </Layout>
<Button cta disabled={error} on:click={save}> <Layout gap="XS" noPadding>
Create super admin user <Button cta disabled={error} on:click={save}>
</Button> Create super admin user
</Button>
{#if multiTenancyEnabled}
<ActionButton
quiet
on:click={() => {
admin.unload()
$goto("../auth/org")
}}
>
Change organisation
</ActionButton>
{/if}
</Layout>
</Layout> </Layout>
</div> </div>
</section> </section>

View File

@ -1,14 +1,18 @@
<script> <script>
import { ActionButton } from "@budibase/bbui" import { ActionButton } from "@budibase/bbui"
import GoogleLogo from "assets/google-logo.png" import GoogleLogo from "assets/google-logo.png"
import { organisation } from "stores/portal" import { auth, organisation } from "stores/portal"
let show
$: tenantId = $auth.tenantId
$: show = $organisation.google $: show = $organisation.google
</script> </script>
{#if show} {#if show}
<ActionButton <ActionButton
on:click={() => window.open("/api/admin/auth/google", "_blank")} on:click={() =>
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
> >
<div class="inner"> <div class="inner">
<img src={GoogleLogo} alt="google icon" /> <img src={GoogleLogo} alt="google icon" />

View File

@ -31,7 +31,7 @@
{#if show} {#if show}
<ActionButton <ActionButton
on:click={() => on:click={() =>
window.open(`/api/admin/auth/oidc/configs/${$oidc.uuid}`, "_blank")} window.open(`/api/global/auth/oidc/configs/${$oidc.uuid}`, "_blank")}
> >
<div class="inner"> <div class="inner">
<img {src} alt="oidc icon" /> <img {src} alt="oidc icon" />

View File

@ -6,10 +6,12 @@
Layout, Layout,
Body, Body,
Heading, Heading,
ActionButton,
} from "@budibase/bbui" } from "@budibase/bbui"
import { organisation, auth } from "stores/portal" import { organisation, auth } from "stores/portal"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte" import { onMount } from "svelte"
import { goto } from "@roxi/routify"
let email = "" let email = ""
@ -41,9 +43,12 @@
</Body> </Body>
<Input label="Email" bind:value={email} /> <Input label="Email" bind:value={email} />
</Layout> </Layout>
<Button cta on:click={forgot} disabled={!email}> <Layout gap="XS" nopadding>
Reset your password <Button cta on:click={forgot} disabled={!email}>
</Button> Reset your password
</Button>
<ActionButton quiet on:click={() => $goto("../")}>Back</ActionButton>
</Layout>
</Layout> </Layout>
</div> </div>
</div> </div>

View File

@ -1,4 +1,24 @@
<script> <script>
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
$redirect("./login") import { auth, admin } from "stores/portal"
import { onMount } from "svelte"
$: tenantSet = $auth.tenantSet
$: multiTenancyEnabled = $admin.multiTenancy
let loaded = false
$: {
if (loaded && multiTenancyEnabled && !tenantSet) {
$redirect("./org")
} else if (loaded) {
$redirect("./login")
}
}
onMount(async () => {
await admin.init()
await auth.checkQueryString()
loaded = true
})
</script> </script>

View File

@ -10,7 +10,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { auth, organisation, oidc } from "stores/portal" import { auth, organisation, oidc, admin } from "stores/portal"
import GoogleButton from "./_components/GoogleButton.svelte" import GoogleButton from "./_components/GoogleButton.svelte"
import OIDCButton from "./_components/OIDCButton.svelte" import OIDCButton from "./_components/OIDCButton.svelte"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
@ -18,8 +18,10 @@
let username = "" let username = ""
let password = "" let password = ""
let loaded = false
$: company = $organisation.company || "Budibase" $: company = $organisation.company || "Budibase"
$: multiTenancyEnabled = $admin.multiTenancy
async function login() { async function login() {
try { try {
@ -27,7 +29,6 @@
username, username,
password, password,
}) })
notifications.success("Logged in successfully")
if ($auth?.user?.forceResetPassword) { if ($auth?.user?.forceResetPassword) {
$goto("./reset") $goto("./reset")
} else { } else {
@ -50,6 +51,7 @@
onMount(async () => { onMount(async () => {
await organisation.init() await organisation.init()
loaded = true
}) })
</script> </script>
@ -61,8 +63,10 @@
<img alt="logo" src={$organisation.logoUrl || Logo} /> <img alt="logo" src={$organisation.logoUrl || Logo} />
<Heading>Sign in to {company}</Heading> <Heading>Sign in to {company}</Heading>
</Layout> </Layout>
<GoogleButton /> {#if loaded}
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} /> <GoogleButton />
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
{/if}
<Divider noGrid /> <Divider noGrid />
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body> <Body size="S" textAlign="center">Sign in with email</Body>
@ -79,6 +83,17 @@
<ActionButton quiet on:click={() => $goto("./forgot")}> <ActionButton quiet on:click={() => $goto("./forgot")}>
Forgot password? Forgot password?
</ActionButton> </ActionButton>
{#if multiTenancyEnabled}
<ActionButton
quiet
on:click={() => {
admin.unload()
$goto("./org")
}}
>
Change organisation
</ActionButton>
{/if}
</Layout> </Layout>
</Layout> </Layout>
</div> </div>

View File

@ -0,0 +1,73 @@
<script>
import { Body, Button, Divider, Heading, Input, Layout } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { auth, admin } from "stores/portal"
import Logo from "assets/bb-emblem.svg"
import { get } from "svelte/store"
import { onMount } from "svelte"
let tenantId = get(auth).tenantSet ? get(auth).tenantId : ""
$: multiTenancyEnabled = $admin.multiTenancy
async function setOrg() {
if (tenantId == null || tenantId === "") {
tenantId = "default"
}
await auth.setOrg(tenantId)
// re-init now org selected
await admin.init()
$goto("../")
}
function handleKeydown(evt) {
if (evt.key === "Enter") setOrg()
}
onMount(async () => {
await auth.checkQueryString()
if (!multiTenancyEnabled) {
$goto("../")
} else {
admin.unload()
}
})
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img alt="logo" src={Logo} />
<Heading>Set Budibase organisation</Heading>
</Layout>
<Divider noGrid />
<Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Set organisation</Body>
<Input label="Organisation" bind:value={tenantId} />
</Layout>
<Layout gap="XS" noPadding>
<Button cta on:click={setOrg}>Set organisation</Button>
</Layout>
</Layout>
</div>
</div>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
img {
width: 48px;
}
</style>

View File

@ -2,13 +2,15 @@
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
import { auth } from "stores/portal" import { auth } from "stores/portal"
auth.checkQueryString()
$: { $: {
if (!$auth.user) { if (!$auth.user) {
$redirect("./auth/login") $redirect(`./auth`)
} else if ($auth.user.builder?.global) { } else if ($auth.user.builder?.global) {
$redirect("./portal") $redirect(`./portal`)
} else { } else {
$redirect("./apps") $redirect(`./apps`)
} }
} }
</script> </script>

View File

@ -7,7 +7,6 @@
import OneLoginLogo from "assets/onelogin-logo.png" import OneLoginLogo from "assets/onelogin-logo.png"
import OidcLogoPng from "assets/oidc-logo.png" import OidcLogoPng from "assets/oidc-logo.png"
import { isEqual, cloneDeep } from "lodash/fp" import { isEqual, cloneDeep } from "lodash/fp"
import { import {
Button, Button,
Heading, Heading,
@ -22,36 +21,51 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import api from "builderStore/api" import api from "builderStore/api"
import { organisation } from "stores/portal" import { organisation, auth, admin } from "stores/portal"
import { uuid } from "builderStore/uuid" import { uuid } from "builderStore/uuid"
$: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy
const ConfigTypes = { const ConfigTypes = {
Google: "google", Google: "google",
OIDC: "oidc", OIDC: "oidc",
// Github: "github",
// AzureAD: "ad",
} }
const GoogleConfigFields = { function callbackUrl(tenantId, end) {
Google: ["clientID", "clientSecret", "callbackURL"], let url = `/api/global/auth`
} if (multiTenancyEnabled && tenantId) {
const GoogleConfigLabels = { url += `/${tenantId}`
Google: { }
clientID: "Client ID", url += end
clientSecret: "Client secret", return url
callbackURL: "Callback URL",
},
} }
const OIDCConfigFields = { $: GoogleConfigFields = {
Oidc: ["configUrl", "clientID", "clientSecret"], Google: [
{ name: "clientID", label: "Client ID" },
{ name: "clientSecret", label: "Client secret" },
{
name: "callbackURL",
label: "Callback URL",
readonly: true,
placeholder: callbackUrl(tenantId, "/google/callback"),
},
],
} }
const OIDCConfigLabels = {
Oidc: { $: OIDCConfigFields = {
configUrl: "Config URL", Oidc: [
clientID: "Client ID", { name: "configUrl", label: "Config URL" },
clientSecret: "Client Secret", { name: "clientID", label: "Client ID" },
}, { name: "clientSecret", label: "Client Secret" },
{
name: "callbackURL",
label: "Callback URL",
readonly: true,
placeholder: callbackUrl(tenantId, "/oidc/callback"),
},
],
} }
let iconDropdownOptions = [ let iconDropdownOptions = [
@ -109,17 +123,13 @@
// Create a flag so that it will only try to save completed forms // Create a flag so that it will only try to save completed forms
$: partialGoogle = $: partialGoogle =
providers.google?.config?.clientID || providers.google?.config?.clientID || providers.google?.config?.clientSecret
providers.google?.config?.clientSecret ||
providers.google?.config?.callbackURL
$: partialOidc = $: partialOidc =
providers.oidc?.config?.configs[0].configUrl || providers.oidc?.config?.configs[0].configUrl ||
providers.oidc?.config?.configs[0].clientID || providers.oidc?.config?.configs[0].clientID ||
providers.oidc?.config?.configs[0].clientSecret providers.oidc?.config?.configs[0].clientSecret
$: googleComplete = $: googleComplete =
providers.google?.config?.clientID && providers.google?.config?.clientID && providers.google?.config?.clientSecret
providers.google?.config?.clientSecret &&
providers.google?.config?.callbackURL
$: oidcComplete = $: oidcComplete =
providers.oidc?.config?.configs[0].configUrl && providers.oidc?.config?.configs[0].configUrl &&
providers.oidc?.config?.configs[0].clientID && providers.oidc?.config?.configs[0].clientID &&
@ -129,7 +139,7 @@
let data = new FormData() let data = new FormData()
data.append("file", file) data.append("file", file)
const res = await api.post( const res = await api.post(
`/api/admin/configs/upload/logos_oidc/${file.name}`, `/api/global/configs/upload/logos_oidc/${file.name}`,
data, data,
{} {}
) )
@ -149,17 +159,21 @@
let calls = [] let calls = []
docs.forEach(element => { docs.forEach(element => {
if (element.type === ConfigTypes.OIDC) { if (element.type === ConfigTypes.OIDC) {
//Add a UUID here so each config is distinguishable when it arrives at the login page. //Add a UUID here so each config is distinguishable when it arrives at the login page
element.config.configs.forEach(config => { for (let config of element.config.configs) {
!config.uuid && (config.uuid = uuid()) if (!config.uuid) {
}) config.uuid = uuid()
}
// callback urls shouldn't be included
delete config.callbackURL
}
if (partialOidc) { if (partialOidc) {
if (!oidcComplete) { if (!oidcComplete) {
notifications.error( notifications.error(
`Please fill in all required ${ConfigTypes.OIDC} fields` `Please fill in all required ${ConfigTypes.OIDC} fields`
) )
} else { } else {
calls.push(api.post(`/api/admin/configs`, element)) calls.push(api.post(`/api/global/configs`, element))
// turn the save button grey when clicked // turn the save button grey when clicked
oidcSaveButtonDisabled = true oidcSaveButtonDisabled = true
originalOidcDoc = cloneDeep(providers.oidc) originalOidcDoc = cloneDeep(providers.oidc)
@ -173,7 +187,8 @@
`Please fill in all required ${ConfigTypes.Google} fields` `Please fill in all required ${ConfigTypes.Google} fields`
) )
} else { } else {
calls.push(api.post(`/api/admin/configs`, element)) delete element.config.callbackURL
calls.push(api.post(`/api/global/configs`, element))
googleSaveButtonDisabled = true googleSaveButtonDisabled = true
originalGoogleDoc = cloneDeep(providers.google) originalGoogleDoc = cloneDeep(providers.google)
} }
@ -206,7 +221,7 @@
await organisation.init() await organisation.init()
// fetch the configs for oauth // fetch the configs for oauth
const googleResponse = await api.get( const googleResponse = await api.get(
`/api/admin/configs/${ConfigTypes.Google}` `/api/global/configs/${ConfigTypes.Google}`
) )
const googleDoc = await googleResponse.json() const googleDoc = await googleResponse.json()
@ -227,7 +242,7 @@
//Get the list of user uploaded logos and push it to the dropdown options. //Get the list of user uploaded logos and push it to the dropdown options.
//This needs to be done before the config call so they're available when the dropdown renders //This needs to be done before the config call so they're available when the dropdown renders
const res = await api.get(`/api/admin/configs/logos_oidc`) const res = await api.get(`/api/global/configs/logos_oidc`)
const configSettings = await res.json() const configSettings = await res.json()
if (configSettings.config) { if (configSettings.config) {
@ -242,17 +257,16 @@
}) })
}) })
} }
const oidcResponse = await api.get(`/api/admin/configs/${ConfigTypes.OIDC}`) const oidcResponse = await api.get(
`/api/global/configs/${ConfigTypes.OIDC}`
)
const oidcDoc = await oidcResponse.json() const oidcDoc = await oidcResponse.json()
if (!oidcDoc._id) { if (!oidcDoc._id) {
console.log("hi")
providers.oidc = { providers.oidc = {
type: ConfigTypes.OIDC, type: ConfigTypes.OIDC,
config: { configs: [{ activated: true }] }, config: { configs: [{ activated: true }] },
} }
} else { } else {
console.log("hello")
originalOidcDoc = cloneDeep(oidcDoc) originalOidcDoc = cloneDeep(oidcDoc)
providers.oidc = oidcDoc providers.oidc = oidcDoc
} }
@ -295,8 +309,12 @@
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#each GoogleConfigFields.Google as field} {#each GoogleConfigFields.Google as field}
<div class="form-row"> <div class="form-row">
<Label size="L">{GoogleConfigLabels.Google[field]}</Label> <Label size="L">{field.label}</Label>
<Input bind:value={providers.google.config[field]} /> <Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
</div> </div>
{/each} {/each}
<div class="form-row"> <div class="form-row">
@ -335,14 +353,14 @@
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#each OIDCConfigFields.Oidc as field} {#each OIDCConfigFields.Oidc as field}
<div class="form-row"> <div class="form-row">
<Label size="L">{OIDCConfigLabels.Oidc[field]}</Label> <Label size="L">{field.label}</Label>
<Input bind:value={providers.oidc.config.configs[0][field]} /> <Input
bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
</div> </div>
{/each} {/each}
<div class="form-row">
<Label size="L">Callback URL</Label>
<Input readonly placeholder="/api/admin/auth/oidc/callback" />
</div>
<br /> <br />
<Body size="S"> <Body size="S">
To customize your login button, fill out the fields below. To customize your login button, fill out the fields below.

View File

@ -53,7 +53,7 @@
delete smtp.config.auth delete smtp.config.auth
} }
// Save your SMTP config // Save your SMTP config
const response = await api.post(`/api/admin/configs`, smtp) const response = await api.post(`/api/global/configs`, smtp)
if (response.status !== 200) { if (response.status !== 200) {
const error = await response.text() const error = await response.text()
@ -75,7 +75,9 @@
async function fetchSmtp() { async function fetchSmtp() {
loading = true loading = true
// fetch the configs for smtp // fetch the configs for smtp
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`) const smtpResponse = await api.get(
`/api/global/configs/${ConfigTypes.SMTP}`
)
const smtpDoc = await smtpResponse.json() const smtpDoc = await smtpResponse.json()
if (!smtpDoc._id) { if (!smtpDoc._id) {
@ -92,8 +94,13 @@
requireAuth = smtpConfig.config.auth != null requireAuth = smtpConfig.config.auth != null
// always attach the auth for the forms purpose - // always attach the auth for the forms purpose -
// this will be removed later if required // this will be removed later if required
smtpConfig.config.auth = { if (!smtpDoc.config) {
type: "login", smtpDoc.config = {}
}
if (!smtpDoc.config.auth) {
smtpConfig.config.auth = {
type: "login",
}
} }
} }

View File

@ -47,8 +47,8 @@
}) })
let selectedApp let selectedApp
const userFetch = fetchData(`/api/admin/users/${userId}`) const userFetch = fetchData(`/api/global/users/${userId}`)
const apps = fetchData(`/api/admin/roles`) const apps = fetchData(`/api/global/roles`)
async function deleteUser() { async function deleteUser() {
const res = await users.delete(userId) const res = await users.delete(userId)

View File

@ -37,7 +37,7 @@
async function uploadLogo(file) { async function uploadLogo(file) {
let data = new FormData() let data = new FormData()
data.append("file", file) data.append("file", file)
const res = await post("/api/admin/configs/upload/settings/logo", data, {}) const res = await post("/api/global/configs/upload/settings/logo", data, {})
return await res.json() return await res.json()
} }

View File

@ -1,4 +1,11 @@
<script> <script>
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
$redirect("./builder") import { auth } from "../stores/portal"
import { onMount } from "svelte"
auth.checkQueryString()
onMount(() => {
$redirect(`./builder`)
})
</script> </script>

View File

@ -1,12 +1,18 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
import api from "builderStore/api" import api from "builderStore/api"
import { auth } from "stores/portal"
export function createAdminStore() { export function createAdminStore() {
const { subscribe, set } = writable({}) const admin = writable({
loaded: false,
})
async function init() { async function init() {
try { try {
const response = await api.get("/api/admin/configs/checklist") const tenantId = get(auth).tenantId
const response = await api.get(
`/api/global/configs/checklist?tenantId=${tenantId}`
)
const json = await response.json() const json = await response.json()
const onboardingSteps = Object.keys(json) const onboardingSteps = Object.keys(json)
@ -16,20 +22,49 @@ export function createAdminStore() {
0 0
) )
set({ await multiTenancyEnabled()
checklist: json, admin.update(store => {
onboardingProgress: (stepsComplete / onboardingSteps.length) * 100, store.loaded = true
store.checklist = json
store.onboardingProgress =
(stepsComplete / onboardingSteps.length) * 100
return store
}) })
} catch (err) { } catch (err) {
set({ admin.update(store => {
checklist: null, store.checklist = null
return store
}) })
} }
} }
async function multiTenancyEnabled() {
let enabled = false
try {
const response = await api.get(`/api/system/flags`)
const json = await response.json()
enabled = json.multiTenancy
} catch (err) {
// just let it stay disabled
}
admin.update(store => {
store.multiTenancy = enabled
return store
})
return enabled
}
function unload() {
admin.update(store => {
store.loaded = false
return store
})
}
return { return {
subscribe, subscribe: admin.subscribe,
init, init,
unload,
} }
} }

View File

@ -1,74 +1,124 @@
import { derived, writable, get } from "svelte/store" import { derived, writable, get } from "svelte/store"
import api from "../../builderStore/api" import api from "../../builderStore/api"
import { admin } from "stores/portal"
export function createAuthStore() { export function createAuthStore() {
const user = writable(null) const auth = writable({
const store = derived(user, $user => { user: null,
tenantId: "default",
tenantSet: false,
loaded: false,
})
const store = derived(auth, $store => {
let initials = null let initials = null
let isAdmin = false let isAdmin = false
let isBuilder = false let isBuilder = false
if ($user) { if ($store.user) {
if ($user.firstName) { const user = $store.user
initials = $user.firstName[0] if (user.firstName) {
if ($user.lastName) { initials = user.firstName[0]
initials += $user.lastName[0] if (user.lastName) {
initials += user.lastName[0]
} }
} else if ($user.email) { } else if (user.email) {
initials = $user.email[0] initials = user.email[0]
} else { } else {
initials = "Unknown" initials = "Unknown"
} }
isAdmin = !!$user.admin?.global isAdmin = !!user.admin?.global
isBuilder = !!$user.builder?.global isBuilder = !!user.builder?.global
} }
return { return {
user: $user, user: $store.user,
tenantId: $store.tenantId,
tenantSet: $store.tenantSet,
loaded: $store.loaded,
initials, initials,
isAdmin, isAdmin,
isBuilder, isBuilder,
} }
}) })
function setUser(user) {
auth.update(store => {
store.loaded = true
store.user = user
if (user) {
store.tenantId = user.tenantId || "default"
store.tenantSet = true
}
return store
})
}
async function setOrganisation(tenantId) {
const prevId = get(store).tenantId
auth.update(store => {
store.tenantId = tenantId
store.tenantSet = !!tenantId
return store
})
if (prevId !== tenantId) {
// re-init admin after setting org
await admin.init()
}
}
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
checkQueryString: async () => {
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has("tenantId")) {
const tenantId = urlParams.get("tenantId")
await setOrganisation(tenantId)
}
},
setOrg: async tenantId => {
await setOrganisation(tenantId)
},
checkAuth: async () => { checkAuth: async () => {
const response = await api.get("/api/admin/users/self") const response = await api.get("/api/global/users/self")
if (response.status !== 200) { if (response.status !== 200) {
user.set(null) setUser(null)
} else { } else {
const json = await response.json() const json = await response.json()
user.set(json) setUser(json)
} }
}, },
login: async creds => { login: async creds => {
const response = await api.post(`/api/admin/auth`, creds) const tenantId = get(store).tenantId
const response = await api.post(
`/api/global/auth/${tenantId}/login`,
creds
)
const json = await response.json() const json = await response.json()
if (response.status === 200) { if (response.status === 200) {
user.set(json.user) setUser(json.user)
} else { } else {
throw "Invalid credentials" throw "Invalid credentials"
} }
return json return json
}, },
logout: async () => { logout: async () => {
const response = await api.post(`/api/admin/auth/logout`) const response = await api.post(`/api/global/auth/logout`)
if (response.status !== 200) { if (response.status !== 200) {
throw "Unable to create logout" throw "Unable to create logout"
} }
await response.json() await response.json()
user.set(null) setUser(null)
}, },
updateSelf: async fields => { updateSelf: async fields => {
const newUser = { ...get(user), ...fields } const newUser = { ...get(auth).user, ...fields }
const response = await api.post("/api/admin/users/self", newUser) const response = await api.post("/api/global/users/self", newUser)
if (response.status === 200) { if (response.status === 200) {
user.set(newUser) setUser(newUser)
} else { } else {
throw "Unable to update user details" throw "Unable to update user details"
} }
}, },
forgotPassword: async email => { forgotPassword: async email => {
const response = await api.post(`/api/admin/auth/reset`, { const tenantId = get(store).tenantId
const response = await api.post(`/api/global/auth/${tenantId}/reset`, {
email, email,
}) })
if (response.status !== 200) { if (response.status !== 200) {
@ -77,17 +127,21 @@ export function createAuthStore() {
await response.json() await response.json()
}, },
resetPassword: async (password, code) => { resetPassword: async (password, code) => {
const response = await api.post(`/api/admin/auth/reset/update`, { const tenantId = get(store).tenantId
password, const response = await api.post(
resetCode: code, `/api/global/auth/${tenantId}/reset/update`,
}) {
password,
resetCode: code,
}
)
if (response.status !== 200) { if (response.status !== 200) {
throw "Unable to reset password" throw "Unable to reset password"
} }
await response.json() await response.json()
}, },
createUser: async user => { createUser: async user => {
const response = await api.post(`/api/admin/users`, user) const response = await api.post(`/api/global/users`, user)
if (response.status !== 200) { if (response.status !== 200) {
throw "Unable to create user" throw "Unable to create user"
} }

View File

@ -9,11 +9,11 @@ export function createEmailStore() {
templates: { templates: {
fetch: async () => { fetch: async () => {
// fetch the email template definitions // fetch the email template definitions
const response = await api.get(`/api/admin/template/definitions`) const response = await api.get(`/api/global/template/definitions`)
const definitions = await response.json() const definitions = await response.json()
// fetch the email templates themselves // fetch the email templates themselves
const templatesResponse = await api.get(`/api/admin/template/email`) const templatesResponse = await api.get(`/api/global/template/email`)
const templates = await templatesResponse.json() const templates = await templatesResponse.json()
store.set({ store.set({
@ -23,7 +23,7 @@ export function createEmailStore() {
}, },
save: async template => { save: async template => {
// Save your template config // Save your template config
const response = await api.post(`/api/admin/template`, template) const response = await api.post(`/api/global/template`, template)
const json = await response.json() const json = await response.json()
if (response.status !== 200) throw new Error(json.message) if (response.status !== 200) throw new Error(json.message)
template._rev = json._rev template._rev = json._rev

View File

@ -1,5 +1,6 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
import api from "builderStore/api" import api from "builderStore/api"
import { auth } from "stores/portal"
const OIDC_CONFIG = { const OIDC_CONFIG = {
logo: undefined, logo: undefined,
@ -12,10 +13,13 @@ export function createOidcStore() {
const { set, subscribe } = store const { set, subscribe } = store
async function init() { async function init() {
const res = await api.get(`/api/admin/configs/publicOidc`) const tenantId = get(auth).tenantId
const res = await api.get(
`/api/global/configs/public/oidc?tenantId=${tenantId}`
)
const json = await res.json() const json = await res.json()
if (json.status === 400) { if (json.status === 400 || Object.keys(json).length === 0) {
set(OIDC_CONFIG) set(OIDC_CONFIG)
} else { } else {
// Just use the first config for now. We will be support multiple logins buttons later on. // Just use the first config for now. We will be support multiple logins buttons later on.

View File

@ -1,8 +1,9 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import api from "builderStore/api" import api from "builderStore/api"
import { auth } from "stores/portal"
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
platformUrl: "http://localhost:1000", platformUrl: "http://localhost:10000",
logoUrl: undefined, logoUrl: undefined,
docsUrl: undefined, docsUrl: undefined,
company: "Budibase", company: "Budibase",
@ -15,7 +16,8 @@ export function createOrganisationStore() {
const { subscribe, set } = store const { subscribe, set } = store
async function init() { async function init() {
const res = await api.get(`/api/admin/configs/public`) const tenantId = get(auth).tenantId
const res = await api.get(`/api/global/configs/public?tenantId=${tenantId}`)
const json = await res.json() const json = await res.json()
if (json.status === 400) { if (json.status === 400) {
@ -26,7 +28,7 @@ export function createOrganisationStore() {
} }
async function save(config) { async function save(config) {
const res = await api.post("/api/admin/configs", { const res = await api.post("/api/global/configs", {
type: "settings", type: "settings",
config: { ...get(store), ...config }, config: { ...get(store), ...config },
_rev: get(store)._rev, _rev: get(store)._rev,

View File

@ -6,7 +6,7 @@ export function createUsersStore() {
const { subscribe, set } = writable([]) const { subscribe, set } = writable([])
async function init() { async function init() {
const response = await api.get(`/api/admin/users`) const response = await api.get(`/api/global/users`)
const json = await response.json() const json = await response.json()
set(json) set(json)
} }
@ -23,12 +23,12 @@ export function createUsersStore() {
global: true, global: true,
} }
} }
const response = await api.post(`/api/admin/users/invite`, body) const response = await api.post(`/api/global/users/invite`, body)
return await response.json() return await response.json()
} }
async function acceptInvite(inviteCode, password) { async function acceptInvite(inviteCode, password) {
const response = await api.post("/api/admin/users/invite/accept", { const response = await api.post("/api/global/users/invite/accept", {
inviteCode, inviteCode,
password, password,
}) })
@ -47,20 +47,20 @@ export function createUsersStore() {
if (admin) { if (admin) {
body.admin = { global: true } body.admin = { global: true }
} }
const response = await api.post("/api/admin/users", body) const response = await api.post("/api/global/users", body)
await init() await init()
return await response.json() return await response.json()
} }
async function del(id) { async function del(id) {
const response = await api.delete(`/api/admin/users/${id}`) const response = await api.delete(`/api/global/users/${id}`)
update(users => users.filter(user => user._id !== id)) update(users => users.filter(user => user._id !== id))
return await response.json() return await response.json()
} }
async function save(data) { async function save(data) {
try { try {
const res = await post(`/api/admin/users`, data) const res = await post(`/api/global/users`, data)
return await res.json() return await res.json()
} catch (error) { } catch (error) {
console.log(error) console.log(error)

View File

@ -13,7 +13,7 @@ export const logIn = async ({ email, password }) => {
return API.error("Please enter your password") return API.error("Please enter your password")
} }
return await API.post({ return await API.post({
url: "/api/admin/auth", url: "/api/global/auth",
body: { username: email, password }, body: { username: email, password },
}) })
} }
@ -23,7 +23,7 @@ export const logIn = async ({ email, password }) => {
*/ */
export const fetchSelf = async () => { export const fetchSelf = async () => {
const user = await API.get({ url: "/api/self" }) const user = await API.get({ url: "/api/self" })
if (user?._id) { if (user && user._id) {
if (user.roleId === "PUBLIC") { if (user.roleId === "PUBLIC") {
// Don't try to enrich a public user as it will 403 // Don't try to enrich a public user as it will 403
return user return user

View File

@ -16,7 +16,7 @@ module FetchMock {
} }
} }
if (url.includes("/api/admin")) { if (url.includes("/api/global")) {
return json({ return json({
email: "test@test.com", email: "test@test.com",
_id: "us_test@test.com", _id: "us_test@test.com",

View File

@ -23,7 +23,9 @@
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write", "format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
"lint": "eslint --fix src/", "lint": "eslint --fix src/",
"lint:fix": "yarn run format && yarn run lint", "lint:fix": "yarn run format && yarn run lint",
"initialise": "node scripts/initialise.js" "initialise": "node scripts/initialise.js",
"multi:enable": "node scripts/multiTenancy.js enable",
"multi:disable": "node scripts/multiTenancy.js disable"
}, },
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",
@ -136,7 +138,8 @@
"supertest": "^4.0.2", "supertest": "^4.0.2",
"ts-jest": "^27.0.3", "ts-jest": "^27.0.3",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^4.3.4" "typescript": "^4.3.4",
"update-dotenv": "^1.1.1"
}, },
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
} }

View File

@ -33,26 +33,29 @@ async function init() {
fs.writeFileSync(envoyOutputPath, processStringSync(contents, config)) fs.writeFileSync(envoyOutputPath, processStringSync(contents, config))
const envFilePath = path.join(process.cwd(), ".env") const envFilePath = path.join(process.cwd(), ".env")
const envFileJson = { if (!fs.existsSync(envFilePath)) {
PORT: 4001, const envFileJson = {
MINIO_URL: "http://localhost:10000/", PORT: 4001,
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/", MINIO_URL: "http://localhost:10000/",
REDIS_URL: "localhost:6379", COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
WORKER_URL: "http://localhost:4002", REDIS_URL: "localhost:6379",
INTERNAL_API_KEY: "budibase", WORKER_URL: "http://localhost:4002",
JWT_SECRET: "testsecret", INTERNAL_API_KEY: "budibase",
REDIS_PASSWORD: "budibase", JWT_SECRET: "testsecret",
MINIO_ACCESS_KEY: "budibase", REDIS_PASSWORD: "budibase",
MINIO_SECRET_KEY: "budibase", MINIO_ACCESS_KEY: "budibase",
COUCH_DB_PASSWORD: "budibase", MINIO_SECRET_KEY: "budibase",
COUCH_DB_USER: "budibase", COUCH_DB_PASSWORD: "budibase",
SELF_HOSTED: 1, COUCH_DB_USER: "budibase",
SELF_HOSTED: 1,
MULTI_TENANCY: "",
}
let envFile = ""
Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n`
})
fs.writeFileSync(envFilePath, envFile)
} }
let envFile = ""
Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n`
})
fs.writeFileSync(envFilePath, envFile)
} }
async function up() { async function up() {

View File

@ -10,10 +10,11 @@ CREATE TABLE Persons (
CREATE TABLE Tasks ( CREATE TABLE Tasks (
TaskID SERIAL PRIMARY KEY, TaskID SERIAL PRIMARY KEY,
PersonID INT, PersonID INT,
Completed BOOLEAN,
TaskName varchar(255), TaskName varchar(255),
CONSTRAINT fkPersons CONSTRAINT fkPersons
FOREIGN KEY(PersonID) FOREIGN KEY(PersonID)
REFERENCES Persons(PersonID) REFERENCES Persons(PersonID)
); );
CREATE TABLE Products ( CREATE TABLE Products (
ProductID SERIAL PRIMARY KEY, ProductID SERIAL PRIMARY KEY,
@ -24,15 +25,15 @@ CREATE TABLE Products_Tasks (
TaskID INT NOT NULL, TaskID INT NOT NULL,
CONSTRAINT fkProducts CONSTRAINT fkProducts
FOREIGN KEY(ProductID) FOREIGN KEY(ProductID)
REFERENCES Products(ProductID), REFERENCES Products(ProductID),
CONSTRAINT fkTasks CONSTRAINT fkTasks
FOREIGN KEY(TaskID) FOREIGN KEY(TaskID)
REFERENCES Tasks(TaskID), REFERENCES Tasks(TaskID),
PRIMARY KEY (ProductID, TaskID) PRIMARY KEY (ProductID, TaskID)
); );
INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast'); INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast');
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling'); INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'assembling', TRUE);
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing'); INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'processing', FALSE);
INSERT INTO Products (ProductName) VALUES ('Computers'); INSERT INTO Products (ProductName) VALUES ('Computers');
INSERT INTO Products (ProductName) VALUES ('Laptops'); INSERT INTO Products (ProductName) VALUES ('Laptops');
INSERT INTO Products (ProductName) VALUES ('Chairs'); INSERT INTO Products (ProductName) VALUES ('Chairs');

View File

@ -0,0 +1,8 @@
#!/usr/bin/env node
const updateDotEnv = require("update-dotenv")
const arg = process.argv.slice(2)[0]
updateDotEnv({
MULTI_TENANCY: arg === "enable" ? "1" : "",
}).then(() => console.log("Updated server!"))

View File

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

View File

@ -25,7 +25,7 @@ const { BASE_LAYOUTS } = require("../../constants/layouts")
const { createHomeScreen } = require("../../constants/screens") const { createHomeScreen } = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
const { processObject } = require("@budibase/string-templates") const { processObject } = require("@budibase/string-templates")
const { getAllApps } = require("../../utilities") const { getAllApps } = require("@budibase/auth/db")
const { USERS_TABLE_SCHEMA } = require("../../constants") const { USERS_TABLE_SCHEMA } = require("../../constants")
const { const {
getDeployedApps, getDeployedApps,
@ -38,6 +38,7 @@ const {
backupClientLibrary, backupClientLibrary,
revertClientLibrary, revertClientLibrary,
} = require("../../utilities/fileSystem/clientLibrary") } = require("../../utilities/fileSystem/clientLibrary")
const { getTenantId, isMultiTenant } = require("@budibase/auth/tenancy")
const URL_REGEX_SLASH = /\/|\\/g const URL_REGEX_SLASH = /\/|\\/g
@ -93,7 +94,8 @@ async function getAppUrlIfNotInUse(ctx) {
} }
async function createInstance(template) { async function createInstance(template) {
const baseAppId = generateAppID() const tenantId = isMultiTenant() ? getTenantId() : null
const baseAppId = generateAppID(tenantId)
const appId = generateDevAppID(baseAppId) const appId = generateDevAppID(baseAppId)
const db = new CouchDB(appId) const db = new CouchDB(appId)
@ -128,7 +130,7 @@ async function createInstance(template) {
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
const dev = ctx.query && ctx.query.status === AppStatus.DEV const dev = ctx.query && ctx.query.status === AppStatus.DEV
const all = ctx.query && ctx.query.status === AppStatus.ALL const all = ctx.query && ctx.query.status === AppStatus.ALL
const apps = await getAllApps({ CouchDB, dev, all }) const apps = await getAllApps(CouchDB, { dev, all })
// get the locks for all the dev apps // get the locks for all the dev apps
if (dev || all) { if (dev || all) {
@ -220,10 +222,12 @@ exports.create = async function (ctx) {
url: url, url: url,
template: ctx.request.body.template, template: ctx.request.body.template,
instance: instance, instance: instance,
tenantId: getTenantId(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
} }
await db.put(newApplication, { force: true }) const response = await db.put(newApplication, { force: true })
newApplication._rev = response.rev
await createEmptyAppPackage(ctx, newApplication) await createEmptyAppPackage(ctx, newApplication)
/* istanbul ignore next */ /* istanbul ignore next */
@ -295,7 +299,7 @@ exports.delete = async function (ctx) {
await deleteApp(ctx.params.appId) await deleteApp(ctx.params.appId)
} }
// make sure the app/role doesn't stick around after the app has been deleted // make sure the app/role doesn't stick around after the app has been deleted
await removeAppFromUserRoles(ctx.params.appId) await removeAppFromUserRoles(ctx, ctx.params.appId)
ctx.status = 200 ctx.status = 200
ctx.body = result ctx.body = result

View File

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

View File

@ -1,6 +1,6 @@
const PouchDB = require("../../../db") const CouchDB = require("../../../db")
const Deployment = require("./Deployment") const Deployment = require("./Deployment")
const { Replication, StaticDatabases } = require("@budibase/auth/db") const { Replication } = require("@budibase/auth/db")
const { DocumentTypes } = require("../../../db/utils") const { DocumentTypes } = require("../../../db/utils")
// the max time we can wait for an invalidation to complete before considering it failed // the max time we can wait for an invalidation to complete before considering it failed
@ -31,13 +31,14 @@ async function checkAllDeployments(deployments) {
async function storeDeploymentHistory(deployment) { async function storeDeploymentHistory(deployment) {
const appId = deployment.getAppId() const appId = deployment.getAppId()
const deploymentJSON = deployment.getJSON() const deploymentJSON = deployment.getJSON()
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name) const db = new CouchDB(appId)
let deploymentDoc let deploymentDoc
try { try {
deploymentDoc = await db.get(appId) // theres only one deployment doc per app database
deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
} catch (err) { } catch (err) {
deploymentDoc = { _id: appId, history: {} } deploymentDoc = { _id: DocumentTypes.DEPLOYMENTS, history: {} }
} }
const deploymentId = deploymentJSON._id const deploymentId = deploymentJSON._id
@ -67,7 +68,7 @@ async function deployApp(deployment) {
}) })
await replication.replicate() await replication.replicate()
const db = new PouchDB(productionAppId) const db = new CouchDB(productionAppId)
const appDoc = await db.get(DocumentTypes.APP_METADATA) const appDoc = await db.get(DocumentTypes.APP_METADATA)
appDoc.appId = productionAppId appDoc.appId = productionAppId
appDoc.instance._id = productionAppId appDoc.instance._id = productionAppId
@ -98,8 +99,9 @@ async function deployApp(deployment) {
exports.fetchDeployments = async function (ctx) { exports.fetchDeployments = async function (ctx) {
try { try {
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name) const appId = ctx.appId
const deploymentDoc = await db.get(ctx.appId) const db = new CouchDB(appId)
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
const { updated, deployments } = await checkAllDeployments( const { updated, deployments } = await checkAllDeployments(
deploymentDoc, deploymentDoc,
ctx.user ctx.user
@ -115,8 +117,9 @@ exports.fetchDeployments = async function (ctx) {
exports.deploymentProgress = async function (ctx) { exports.deploymentProgress = async function (ctx) {
try { try {
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name) const appId = ctx.appId
const deploymentDoc = await db.get(ctx.appId) const db = new CouchDB(appId)
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
ctx.body = deploymentDoc[ctx.params.deploymentId] ctx.body = deploymentDoc[ctx.params.deploymentId]
} catch (err) { } catch (err) {
ctx.throw( ctx.throw(

View File

@ -9,8 +9,9 @@ const { DocumentTypes } = require("../../db/utils")
async function redirect(ctx, method) { async function redirect(ctx, method) {
const { devPath } = ctx.params const { devPath } = ctx.params
const queryString = ctx.originalUrl.split("?")[1] || ""
const response = await fetch( const response = await fetch(
checkSlashesInUrl(`${env.WORKER_URL}/api/admin/${devPath}`), checkSlashesInUrl(`${env.WORKER_URL}/api/global/${devPath}?${queryString}`),
request( request(
ctx, ctx,
{ {

View File

@ -161,7 +161,7 @@ exports.fetchView = async ctx => {
schema: {}, schema: {},
} }
} }
rows = await outputProcessing(appId, table, response.rows) rows = await outputProcessing(ctx, table, response.rows)
} }
if (calculation === CALCULATION_TYPES.STATS) { if (calculation === CALCULATION_TYPES.STATS) {
@ -204,7 +204,7 @@ exports.fetch = async ctx => {
) )
rows = response.rows.map(row => row.doc) rows = response.rows.map(row => row.doc)
} }
return outputProcessing(appId, table, rows) return outputProcessing(ctx, table, rows)
} }
exports.find = async ctx => { exports.find = async ctx => {
@ -212,7 +212,7 @@ exports.find = async ctx => {
const db = new CouchDB(appId) const db = new CouchDB(appId)
const table = await db.get(ctx.params.tableId) const table = await db.get(ctx.params.tableId)
let row = await findRow(ctx, db, ctx.params.tableId, ctx.params.rowId) let row = await findRow(ctx, db, ctx.params.tableId, ctx.params.rowId)
row = await outputProcessing(appId, table, row) row = await outputProcessing(ctx, table, row)
return row return row
} }
@ -291,7 +291,7 @@ exports.search = async ctx => {
// Enrich search results with relationships // Enrich search results with relationships
if (response.rows && response.rows.length) { if (response.rows && response.rows.length) {
const table = await db.get(tableId) const table = await db.get(tableId)
response.rows = await outputProcessing(appId, table, response.rows) response.rows = await outputProcessing(ctx, table, response.rows)
} }
return response return response
@ -328,7 +328,7 @@ exports.fetchEnrichedRow = async ctx => {
}) })
// need to include the IDs in these rows for any links they may have // need to include the IDs in these rows for any links they may have
let linkedRows = await outputProcessing( let linkedRows = await outputProcessing(
appId, ctx,
table, table,
response.rows.map(row => row.doc) response.rows.map(row => row.doc)
) )

View File

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

View File

@ -1,5 +1,6 @@
const Router = require("@koa/router") 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 currentApp = require("../middleware/currentapp")
const compress = require("koa-compress") const compress = require("koa-compress")
const zlib = require("zlib") const zlib = require("zlib")
@ -9,6 +10,13 @@ const env = require("../environment")
const router = new Router() const router = new Router()
const NO_TENANCY_ENDPOINTS = [
{
route: "/api/analytics",
method: "GET",
},
]
router router
.use( .use(
compress({ compress({
@ -36,6 +44,8 @@ router
publicAllowed: true, publicAllowed: true,
}) })
) )
// nothing in the server should allow query string tenants
.use(buildTenancyMiddleware(null, NO_TENANCY_ENDPOINTS))
.use(currentApp) .use(currentApp)
.use(auditLog) .use(auditLog)

View File

@ -6,11 +6,11 @@ const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()
router router
.post("/api/applications", authorized(BUILDER), controller.create)
.get("/api/applications/:appId/definition", controller.fetchAppDefinition) .get("/api/applications/:appId/definition", controller.fetchAppDefinition)
.get("/api/applications", controller.fetch) .get("/api/applications", controller.fetch)
.get("/api/applications/:appId/appPackage", controller.fetchAppPackage) .get("/api/applications/:appId/appPackage", controller.fetchAppPackage)
.put("/api/applications/:appId", authorized(BUILDER), controller.update) .put("/api/applications/:appId", authorized(BUILDER), controller.update)
.post("/api/applications", authorized(BUILDER), controller.create)
.post( .post(
"/api/applications/:appId/client/update", "/api/applications/:appId/client/update",
authorized(BUILDER), authorized(BUILDER),

View File

@ -8,9 +8,9 @@ const router = Router()
if (env.isDev() || env.isTest()) { if (env.isDev() || env.isTest()) {
router router
.get("/api/admin/:devPath(.*)", controller.redirectGet) .get("/api/global/:devPath(.*)", controller.redirectGet)
.post("/api/admin/:devPath(.*)", controller.redirectPost) .post("/api/global/:devPath(.*)", controller.redirectPost)
.delete("/api/admin/:devPath(.*)", controller.redirectDelete) .delete("/api/global/:devPath(.*)", controller.redirectDelete)
} }
router router

View File

@ -387,7 +387,7 @@ describe("/rows", () => {
}) })
// the environment needs configured for this // the environment needs configured for this
await setup.switchToSelfHosted(async () => { await setup.switchToSelfHosted(async () => {
const enriched = await outputProcessing(config.getAppId(), table, [row]) const enriched = await outputProcessing({ appId: config.getAppId() }, table, [row])
expect(enriched[0].attachment[0].url).toBe( expect(enriched[0].attachment[0].url).toBe(
`/prod-budi-app-assets/${config.getAppId()}/attachments/test/thing.csv` `/prod-budi-app-assets/${config.getAppId()}/attachments/test/thing.csv`
) )

View File

@ -3,6 +3,7 @@ const appController = require("../../../controllers/application")
const CouchDB = require("../../../../db") const CouchDB = require("../../../../db")
const { AppStatus } = require("../../../../db/utils") const { AppStatus } = require("../../../../db/utils")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { TENANT_ID } = require("../../../../tests/utilities/structures")
function Request(appId, params) { function Request(appId, params) {
this.appId = appId this.appId = appId
@ -16,8 +17,8 @@ exports.getAllTableRows = async config => {
return req.body return req.body
} }
exports.clearAllApps = async () => { exports.clearAllApps = async (tenantId = TENANT_ID) => {
const req = { query: { status: AppStatus.DEV } } const req = { query: { status: AppStatus.DEV }, user: { tenantId } }
await appController.fetch(req) await appController.fetch(req)
const apps = req.body const apps = req.body
if (!apps || apps.length <= 0) { if (!apps || apps.length <= 0) {

View File

@ -19,7 +19,7 @@ module.exports.definition = {
properties: { properties: {
text: { text: {
type: "string", type: "string",
title: "URL", title: "Log",
}, },
}, },
required: ["text"], required: ["text"],

View File

@ -3,6 +3,10 @@ const logic = require("./logic")
const automationUtils = require("./automationUtils") const automationUtils = require("./automationUtils")
const AutomationEmitter = require("../events/AutomationEmitter") const AutomationEmitter = require("../events/AutomationEmitter")
const { processObject } = require("@budibase/string-templates") const { processObject } = require("@budibase/string-templates")
const { DEFAULT_TENANT_ID } = require("@budibase/auth").constants
const CouchDB = require("../db")
const { DocumentTypes } = require("../db/utils")
const { doInTenant } = require("@budibase/auth/tenancy")
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
@ -16,6 +20,7 @@ class Orchestrator {
this._metadata = triggerOutput.metadata this._metadata = triggerOutput.metadata
this._chainCount = this._metadata ? this._metadata.automationChainCount : 0 this._chainCount = this._metadata ? this._metadata.automationChainCount : 0
this._appId = triggerOutput.appId this._appId = triggerOutput.appId
this._app = null
// remove from context // remove from context
delete triggerOutput.appId delete triggerOutput.appId
delete triggerOutput.metadata delete triggerOutput.metadata
@ -40,8 +45,19 @@ class Orchestrator {
return step return step
} }
async getApp() {
const appId = this._appId
if (this._app) {
return this._app
}
const db = new CouchDB(appId)
this._app = await db.get(DocumentTypes.APP_METADATA)
return this._app
}
async execute() { async execute() {
let automation = this._automation let automation = this._automation
const app = await this.getApp()
for (let step of automation.definition.steps) { for (let step of automation.definition.steps) {
let stepFn = await this.getStepFunctionality(step.type, step.stepId) let stepFn = await this.getStepFunctionality(step.type, step.stepId)
step.inputs = await processObject(step.inputs, this._context) step.inputs = await processObject(step.inputs, this._context)
@ -51,12 +67,15 @@ class Orchestrator {
) )
// appId is always passed // appId is always passed
try { try {
const outputs = await stepFn({ let tenantId = app.tenantId || DEFAULT_TENANT_ID
inputs: step.inputs, const outputs = await doInTenant(tenantId, () => {
appId: this._appId, return stepFn({
apiKey: automation.apiKey, inputs: step.inputs,
emitter: this._emitter, appId: this._appId,
context: this._context, apiKey: automation.apiKey,
emitter: this._emitter,
context: this._context,
})
}) })
if (step.stepId === FILTER_STEP_ID && !outputs.success) { if (step.stepId === FILTER_STEP_ID && !outputs.success) {
break break

View File

@ -1,38 +0,0 @@
const CouchDB = require("./index")
const { StaticDatabases } = require("./utils")
const env = require("../environment")
const SELF_HOST_ERR = "Unable to access builder DB/doc - not self hosted."
const BUILDER_DB = StaticDatabases.BUILDER
/**
* This is the builder database, right now this is a single, static database
* that is present across the whole system and determines some core functionality
* for the builder (e.g. storage of API keys). This has been limited to self hosting
* as it doesn't make as much sense against the currently design Cloud system.
*/
exports.getBuilderMainDoc = async () => {
if (!env.SELF_HOSTED) {
throw SELF_HOST_ERR
}
const db = new CouchDB(BUILDER_DB.name)
try {
return await db.get(BUILDER_DB.baseDoc)
} catch (err) {
// doesn't exist yet, nothing to get
return {
_id: BUILDER_DB.baseDoc,
}
}
}
exports.setBuilderMainDoc = async doc => {
if (!env.SELF_HOSTED) {
throw SELF_HOST_ERR
}
// make sure to override the ID
doc._id = BUILDER_DB.baseDoc
const db = new CouchDB(BUILDER_DB.name)
return db.put(doc)
}

View File

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

View File

@ -34,6 +34,7 @@ const DocumentTypes = {
DATASOURCE: "datasource", DATASOURCE: "datasource",
DATASOURCE_PLUS: "datasource_plus", DATASOURCE_PLUS: "datasource_plus",
QUERY: "query", QUERY: "query",
DEPLOYMENTS: "deployments",
} }
const ViewNames = { const ViewNames = {
@ -49,13 +50,7 @@ const SearchIndexes = {
ROWS: "rows", ROWS: "rows",
} }
exports.StaticDatabases = { exports.StaticDatabases = StaticDatabases
BUILDER: {
name: "builder-db",
baseDoc: "builder-doc",
},
...StaticDatabases,
}
const BudibaseInternalDB = { const BudibaseInternalDB = {
_id: "bb_internal", _id: "bb_internal",
@ -230,8 +225,12 @@ exports.getLinkParams = (otherProps = {}) => {
* Generates a new app ID. * Generates a new app ID.
* @returns {string} The new app ID which the app doc can be stored under. * @returns {string} The new app ID which the app doc can be stored under.
*/ */
exports.generateAppID = () => { exports.generateAppID = (tenantId = null) => {
return `${DocumentTypes.APP}${SEPARATOR}${newid()}` let id = `${DocumentTypes.APP}${SEPARATOR}`
if (tenantId) {
id += `${tenantId}${SEPARATOR}`
}
return `${id}${newid()}`
} }
/** /**
@ -240,8 +239,8 @@ exports.generateAppID = () => {
*/ */
exports.generateDevAppID = appId => { exports.generateDevAppID = appId => {
const prefix = `${DocumentTypes.APP}${SEPARATOR}` const prefix = `${DocumentTypes.APP}${SEPARATOR}`
const uuid = appId.split(prefix)[1] const rest = appId.split(prefix)[1]
return `${DocumentTypes.APP_DEV}${SEPARATOR}${uuid}` return `${DocumentTypes.APP_DEV}${SEPARATOR}${rest}`
} }
/** /**

View File

@ -35,6 +35,7 @@ module.exports = {
REDIS_URL: process.env.REDIS_URL, REDIS_URL: process.env.REDIS_URL,
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
MULTI_TENANCY: process.env.MULTI_TENANCY,
// environment // environment
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
JEST_WORKER_ID: process.env.JEST_WORKER_ID, JEST_WORKER_ID: process.env.JEST_WORKER_ID,

View File

@ -68,5 +68,6 @@ module.exports = async (ctx, next) => {
) { ) {
setCookie(ctx, { appId }, Cookies.CurrentApp) setCookie(ctx, { appId }, Cookies.CurrentApp)
} }
return next() return next()
} }

View File

@ -10,16 +10,19 @@ const {
basicScreen, basicScreen,
basicLayout, basicLayout,
basicWebhook, basicWebhook,
TENANT_ID,
} = require("./structures") } = require("./structures")
const controllers = require("./controllers") const controllers = require("./controllers")
const supertest = require("supertest") const supertest = require("supertest")
const { cleanup } = require("../../utilities/fileSystem") const { cleanup } = require("../../utilities/fileSystem")
const { Cookies, Headers } = require("@budibase/auth").constants const { Cookies, Headers } = require("@budibase/auth").constants
const { jwt } = require("@budibase/auth").auth const { jwt } = require("@budibase/auth").auth
const { StaticDatabases } = require("@budibase/auth/db") const auth = require("@budibase/auth")
const { getGlobalDB } = require("@budibase/auth/tenancy")
const { createASession } = require("@budibase/auth/sessions") const { createASession } = require("@budibase/auth/sessions")
const { user: userCache } = require("@budibase/auth/cache") const { user: userCache } = require("@budibase/auth/cache")
const CouchDB = require("../../db") const CouchDB = require("../../db")
auth.init(CouchDB)
const GLOBAL_USER_ID = "us_uuid1" const GLOBAL_USER_ID = "us_uuid1"
const EMAIL = "babs@babs.com" const EMAIL = "babs@babs.com"
@ -52,7 +55,7 @@ class TestConfiguration {
request.cookies = { set: () => {}, get: () => {} } request.cookies = { set: () => {}, get: () => {} }
request.config = { jwtSecret: env.JWT_SECRET } request.config = { jwtSecret: env.JWT_SECRET }
request.appId = this.appId request.appId = this.appId
request.user = { appId: this.appId } request.user = { appId: this.appId, tenantId: TENANT_ID }
request.query = {} request.query = {}
request.request = { request.request = {
body: config, body: config,
@ -65,7 +68,7 @@ class TestConfiguration {
} }
async globalUser(id = GLOBAL_USER_ID, builder = true, roles) { async globalUser(id = GLOBAL_USER_ID, builder = true, roles) {
const db = new CouchDB(StaticDatabases.GLOBAL.name) const db = getGlobalDB(TENANT_ID)
let existing let existing
try { try {
existing = await db.get(id) existing = await db.get(id)
@ -76,8 +79,9 @@ class TestConfiguration {
_id: id, _id: id,
...existing, ...existing,
roles: roles || {}, roles: roles || {},
tenantId: TENANT_ID,
} }
await createASession(id, "sessionid") await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID })
if (builder) { if (builder) {
user.builder = { global: true } user.builder = { global: true }
} }
@ -107,6 +111,7 @@ class TestConfiguration {
const auth = { const auth = {
userId: GLOBAL_USER_ID, userId: GLOBAL_USER_ID,
sessionId: "sessionid", sessionId: "sessionid",
tenantId: TENANT_ID,
} }
const app = { const app = {
roleId: BUILTIN_ROLE_IDS.ADMIN, roleId: BUILTIN_ROLE_IDS.ADMIN,
@ -333,11 +338,15 @@ class TestConfiguration {
if (!email || !password) { if (!email || !password) {
await this.createUser() await this.createUser()
} }
await createASession(userId, "sessionid") await createASession(userId, {
sessionId: "sessionid",
tenantId: TENANT_ID,
})
// have to fake this // have to fake this
const auth = { const auth = {
userId, userId,
sessionId: "sessionid", sessionId: "sessionid",
tenantId: TENANT_ID,
} }
const app = { const app = {
roleId: roleId, roleId: roleId,

View File

@ -4,6 +4,8 @@ const { createHomeScreen } = require("../../constants/screens")
const { EMPTY_LAYOUT } = require("../../constants/layouts") const { EMPTY_LAYOUT } = require("../../constants/layouts")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
exports.TENANT_ID = "default"
exports.basicTable = () => { exports.basicTable = () => {
return { return {
name: "TestTable", name: "TestTable",

View File

@ -1,13 +1,12 @@
const CouchDB = require("../db")
const { const {
getMultiIDParams, getMultiIDParams,
getGlobalIDFromUserMetadataID, getGlobalIDFromUserMetadataID,
StaticDatabases,
} = require("../db/utils") } = require("../db/utils")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { getDeployedAppID } = require("@budibase/auth/db") const { getDeployedAppID } = require("@budibase/auth/db")
const { getGlobalUserParams } = require("@budibase/auth/db") const { getGlobalUserParams } = require("@budibase/auth/db")
const { user: userCache } = require("@budibase/auth/cache") const { user: userCache } = require("@budibase/auth/cache")
const { getGlobalDB } = require("@budibase/auth/tenancy")
exports.updateAppRole = (appId, user) => { exports.updateAppRole = (appId, user) => {
if (!user.roles) { if (!user.roles) {
@ -34,18 +33,20 @@ function processUser(appId, user) {
} }
exports.getCachedSelf = async (ctx, appId) => { exports.getCachedSelf = async (ctx, appId) => {
// this has to be tenant aware, can't depend on the context to find it out
// running some middlewares before the tenancy causes context to break
const user = await userCache.getUser(ctx.user._id) const user = await userCache.getUser(ctx.user._id)
return processUser(appId, user) return processUser(appId, user)
} }
exports.getGlobalUser = async (appId, userId) => { exports.getGlobalUser = async (ctx, appId, userId) => {
const db = CouchDB(StaticDatabases.GLOBAL.name) const db = getGlobalDB()
let user = await db.get(getGlobalIDFromUserMetadataID(userId)) let user = await db.get(getGlobalIDFromUserMetadataID(userId))
return processUser(appId, user) return processUser(appId, user)
} }
exports.getGlobalUsers = async (appId = null, users = null) => { exports.getGlobalUsers = async (ctx, appId = null, users = null) => {
const db = CouchDB(StaticDatabases.GLOBAL.name) const db = getGlobalDB()
let globalUsers let globalUsers
if (users) { if (users) {
const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id)) const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id))

View File

@ -1,6 +1,5 @@
const env = require("../environment") const env = require("../environment")
const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants") const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants")
const { getAllApps } = require("@budibase/auth/db")
const { sanitizeKey } = require("@budibase/auth/src/objectStore") const { sanitizeKey } = require("@budibase/auth/src/objectStore")
const BB_CDN = "https://cdn.app.budi.live/assets" const BB_CDN = "https://cdn.app.budi.live/assets"
@ -8,7 +7,6 @@ const BB_CDN = "https://cdn.app.budi.live/assets"
exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms)) exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms))
exports.isDev = env.isDev exports.isDev = env.isDev
exports.getAllApps = getAllApps
/** /**
* Makes sure that a URL has the correct number of slashes, while maintaining the * Makes sure that a URL has the correct number of slashes, while maintaining the

View File

@ -7,8 +7,10 @@ let devAppClient, debounceClient
// we init this as we want to keep the connection open all the time // we init this as we want to keep the connection open all the time
// reduces the performance hit // reduces the performance hit
exports.init = async () => { exports.init = async () => {
devAppClient = await new Client(utils.Databases.DEV_LOCKS).init() devAppClient = new Client(utils.Databases.DEV_LOCKS)
debounceClient = await new Client(utils.Databases.DEBOUNCE).init() debounceClient = new Client(utils.Databases.DEBOUNCE)
await devAppClient.init()
await debounceClient.init()
} }
exports.shutdown = async () => { exports.shutdown = async () => {

View File

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

View File

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

View File

@ -4,13 +4,17 @@ const { checkSlashesInUrl } = require("./index")
const { getDeployedAppID } = require("@budibase/auth/db") const { getDeployedAppID } = require("@budibase/auth/db")
const { updateAppRole, getGlobalUser } = require("./global") const { updateAppRole, getGlobalUser } = require("./global")
const { Headers } = require("@budibase/auth/constants") const { Headers } = require("@budibase/auth/constants")
const { getTenantId, isTenantIdSet } = require("@budibase/auth/tenancy")
function request(ctx, request, noApiKey) { function request(ctx, request) {
if (!request.headers) { if (!request.headers) {
request.headers = {} request.headers = {}
} }
if (!noApiKey) { if (!ctx) {
request.headers[Headers.API_KEY] = env.INTERNAL_API_KEY request.headers[Headers.API_KEY] = env.INTERNAL_API_KEY
if (isTenantIdSet()) {
request.headers[Headers.TENANT_ID] = getTenantId()
}
} }
if (request.body && Object.keys(request.body).length > 0) { if (request.body && Object.keys(request.body).length > 0) {
request.headers["Content-Type"] = "application/json" request.headers["Content-Type"] = "application/json"
@ -29,9 +33,11 @@ function request(ctx, request, noApiKey) {
exports.request = request exports.request = request
// have to pass in the tenant ID as this could be coming from an automation
exports.sendSmtpEmail = async (to, from, subject, contents) => { exports.sendSmtpEmail = async (to, from, subject, contents) => {
// tenant ID will be set in header
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`), checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
request(null, { request(null, {
method: "POST", method: "POST",
body: { body: {
@ -74,11 +80,11 @@ exports.getDeployedApps = async ctx => {
} }
exports.getGlobalSelf = async (ctx, appId = null) => { exports.getGlobalSelf = async (ctx, appId = null) => {
const endpoint = `/api/admin/users/self` const endpoint = `/api/global/users/self`
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint), checkSlashesInUrl(env.WORKER_URL + endpoint),
// we don't want to use API key when getting self // we don't want to use API key when getting self
request(ctx, { method: "GET" }, true) request(ctx, { method: "GET" })
) )
if (response.status !== 200) { if (response.status !== 200) {
ctx.throw(400, "Unable to get self globally.") ctx.throw(400, "Unable to get self globally.")
@ -96,11 +102,11 @@ exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => {
body = {} body = {}
if (!userId) { if (!userId) {
user = await exports.getGlobalSelf(ctx) user = await exports.getGlobalSelf(ctx)
endpoint = `/api/admin/users/self` endpoint = `/api/global/users/self`
} else { } else {
user = await getGlobalUser(appId, userId) user = await getGlobalUser(ctx, appId, userId)
body._id = userId body._id = userId
endpoint = `/api/admin/users` endpoint = `/api/global/users`
} }
body = { body = {
...body, ...body,
@ -122,11 +128,11 @@ exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => {
return response.json() return response.json()
} }
exports.removeAppFromUserRoles = async appId => { exports.removeAppFromUserRoles = async (ctx, appId) => {
const deployedAppId = getDeployedAppID(appId) const deployedAppId = getDeployedAppID(appId)
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/admin/roles/${deployedAppId}`), checkSlashesInUrl(env.WORKER_URL + `/api/global/roles/${deployedAppId}`),
request(null, { request(ctx, {
method: "DELETE", method: "DELETE",
}) })
) )

View File

@ -1146,12 +1146,11 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/auth@^0.9.80-alpha.7": "@budibase/auth@^0.9.79-alpha.4":
version "0.9.80-alpha.7" version "0.9.79"
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.80-alpha.7.tgz#6fb4c40a5f437bb9f7e49c9acafbc601b0dffa49" resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.79.tgz#416271ffc55e84116550469656bf151a7734a90f"
integrity sha512-9KZy8hqdpaWRY2n3pRAThP4Jb9TsrfJsJFdfDndJtPO1tTNKtDw2LGEwrT5Kym0a0SBHEzVrXq1Vw/sg72ACIQ== integrity sha512-ENh099tYeUfVExsAeoxwMh2ODioKQGPteK9LJiU5hMdM4Oi7pyImu287BgKpTIheB+WtadT4e21VpPaJ62APEw==
dependencies: dependencies:
"@techpass/passport-openidconnect" "^0.3.0"
aws-sdk "^2.901.0" aws-sdk "^2.901.0"
bcryptjs "^2.4.3" bcryptjs "^2.4.3"
ioredis "^4.27.1" ioredis "^4.27.1"
@ -1168,10 +1167,10 @@
uuid "^8.3.2" uuid "^8.3.2"
zlib "^1.0.5" zlib "^1.0.5"
"@budibase/bbui@^0.9.80-alpha.7": "@budibase/bbui@^0.9.79":
version "0.9.80-alpha.7" version "0.9.79"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.80-alpha.7.tgz#5fbb7a6617a35a560151377fdc67a845f5620803" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.79.tgz#c033ba0af41cb584d2657a8353f9887328f6633f"
integrity sha512-VJPP6A3BhxsLQzEfKPz3alCiT0nMqeM75P/reT1jsRxZsOCJ8vFn7g2c8aH2bEIcCqOWeUaaxVDuj8ghbzByUw== integrity sha512-XxUJSPGd2FZDFdbNOeMUXohhID5h3DVq9XyKTe6WhYax4m2da/2WTENJ16UFvmfA+yxLN1qSDeweq9vw2zCahQ==
dependencies: dependencies:
"@adobe/spectrum-css-workflow-icons" "^1.2.1" "@adobe/spectrum-css-workflow-icons" "^1.2.1"
"@spectrum-css/actionbutton" "^1.0.1" "@spectrum-css/actionbutton" "^1.0.1"
@ -1216,14 +1215,14 @@
svelte-flatpickr "^3.1.0" svelte-flatpickr "^3.1.0"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/client@^0.9.80-alpha.7": "@budibase/client@^0.9.79-alpha.4":
version "0.9.80-alpha.7" version "0.9.79"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.80-alpha.7.tgz#9d2e98b90cd9fdcfc659826d19b5dc206cdcfe7d" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.79.tgz#d1c8d51e9121f81902cfb31d3b685c8061f272a2"
integrity sha512-szLz2JpWI9ZMyVz7IPap1fQ7e+uphuthOkOsERplmq4EXbv914/YILdEfUm01s4aeOEOdkeogz31t8t75es6Dg== integrity sha512-//Yqm5Qki6BmBe5W2Tz8GONdkFjdD1jkIU7pcLYKqdZJWEQIrX6T/xNvYvZVhw7Dx5bwSZRjFwzm7jLoiyHBIA==
dependencies: dependencies:
"@budibase/bbui" "^0.9.80-alpha.7" "@budibase/bbui" "^0.9.79"
"@budibase/standard-components" "^0.9.80-alpha.7" "@budibase/standard-components" "^0.9.79"
"@budibase/string-templates" "^0.9.80-alpha.7" "@budibase/string-templates" "^0.9.79"
regexparam "^1.3.0" regexparam "^1.3.0"
shortid "^2.2.15" shortid "^2.2.15"
svelte-spa-router "^3.0.5" svelte-spa-router "^3.0.5"
@ -1256,26 +1255,24 @@
to-gfm-code-block "^0.1.1" to-gfm-code-block "^0.1.1"
year "^0.2.1" year "^0.2.1"
"@budibase/standard-components@^0.9.80-alpha.7": "@budibase/standard-components@^0.9.79", "@budibase/standard-components@^0.9.79-alpha.4":
version "0.9.80-alpha.7" version "0.9.79"
resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.80-alpha.7.tgz#17f13a25bfcda873f44d1460493325adcfe6f188" resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.79.tgz#24206642e0cdc655ea3a99ed5e9402ec4f6b3ba8"
integrity sha512-ohEVqhRxp2FeOlEnJtfBhyqtwmRGI/qPGs0K9FQfLQglMYJtPN5FgMrJ1gtN0W3zn7TOfNFnTcQIxIdLxSLwyA== integrity sha512-ZWhmBZ1iG+CjGMEvT/jtugMMgA1n88UYcOfP3BSP2P3eA16DubyU9hH9OyJHbGPzDHLoBF6vuS/5ZPZCkOKppw==
dependencies: dependencies:
"@budibase/bbui" "^0.9.80-alpha.7" "@budibase/bbui" "^0.9.79"
"@spectrum-css/card" "^3.0.3"
"@spectrum-css/link" "^3.1.3" "@spectrum-css/link" "^3.1.3"
"@spectrum-css/page" "^3.0.1" "@spectrum-css/page" "^3.0.1"
"@spectrum-css/typography" "^3.0.2"
"@spectrum-css/vars" "^3.0.1" "@spectrum-css/vars" "^3.0.1"
apexcharts "^3.22.1" apexcharts "^3.22.1"
dayjs "^1.10.5" dayjs "^1.10.5"
svelte-apexcharts "^1.0.2" svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0" svelte-flatpickr "^3.1.0"
"@budibase/string-templates@^0.9.80-alpha.7": "@budibase/string-templates@^0.9.79", "@budibase/string-templates@^0.9.79-alpha.4":
version "0.9.80-alpha.7" version "0.9.79"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.80-alpha.7.tgz#10b06fc8652c00065f8928caebfcd0d143660078" resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.79.tgz#bb75a7433a7cfda1fc488283f35e47879b799fcc"
integrity sha512-lD3BSWXW6PrdAbZcpVXSsr/fA8NdwvQ8W7T4chQ661UUMKVOYLnGwAvvAOArGpkdzSOAfSEuzgIB0+pBc92qWQ== integrity sha512-hkAne5mx7mj8+osXFt45VwgLKSa94uQOGOb4R8uv9WNzvk4RzcjBfRzJxggv29FUemItrAeZpSh+Um6yugFI+w==
dependencies: dependencies:
"@budibase/handlebars-helpers" "^0.11.4" "@budibase/handlebars-helpers" "^0.11.4"
dayjs "^1.10.4" dayjs "^1.10.4"
@ -2114,11 +2111,6 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/buttongroup/-/buttongroup-3.0.3.tgz#719d868845ac9d2c4f939c1b9f6044507902d5aa" resolved "https://registry.yarnpkg.com/@spectrum-css/buttongroup/-/buttongroup-3.0.3.tgz#719d868845ac9d2c4f939c1b9f6044507902d5aa"
integrity sha512-eXl8U4QWMWXqyTu654FdQdEGnmczgOYlpIFSHyCMVjhtPqZp2xwnLFiGh6LKw+bLio6eeOZ0L+vpk1GcoYqgkw== integrity sha512-eXl8U4QWMWXqyTu654FdQdEGnmczgOYlpIFSHyCMVjhtPqZp2xwnLFiGh6LKw+bLio6eeOZ0L+vpk1GcoYqgkw==
"@spectrum-css/card@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@spectrum-css/card/-/card-3.0.3.tgz#56b2e2da6b80c1583228baa279de7407383bfb6b"
integrity sha512-+oKLUI2a0QmQP9EzySeq/G4FpUkkdaDNbuEbqCj2IkPMc/2v/nwzsPhh1fj2UIghGAiiUwXfPpzax1e8fyhQUg==
"@spectrum-css/checkbox@^3.0.2": "@spectrum-css/checkbox@^3.0.2":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.3.tgz#8577067fc8b97e4609f92bd242364937a533a7bb" resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.3.tgz#8577067fc8b97e4609f92bd242364937a533a7bb"
@ -2278,7 +2270,7 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/treeview/-/treeview-3.0.3.tgz#aeda5175158b9f8d7529cb2b394428eb2a428046" resolved "https://registry.yarnpkg.com/@spectrum-css/treeview/-/treeview-3.0.3.tgz#aeda5175158b9f8d7529cb2b394428eb2a428046"
integrity sha512-D5gGzZC/KtRArdx86Mesc9+99W9nTbUOeyYGqoJoAfJSOttoT6Tk5CrDvlCmAqjKf5rajemAkGri1ChqvUIwkw== integrity sha512-D5gGzZC/KtRArdx86Mesc9+99W9nTbUOeyYGqoJoAfJSOttoT6Tk5CrDvlCmAqjKf5rajemAkGri1ChqvUIwkw==
"@spectrum-css/typography@^3.0.1", "@spectrum-css/typography@^3.0.2": "@spectrum-css/typography@^3.0.1":
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/typography/-/typography-3.0.2.tgz#ea3ca0a60e18064527819d48c8c4364cab4fcd38" resolved "https://registry.yarnpkg.com/@spectrum-css/typography/-/typography-3.0.2.tgz#ea3ca0a60e18064527819d48c8c4364cab4fcd38"
integrity sha512-5ZOLmQe0edzsDMyhghUd4hBb5uxGsFrxzf+WasfcUw9klSfTsRZ09n1BsaaWbgrLjlMQ+EEHS46v5VNo0Ms2CA== integrity sha512-5ZOLmQe0edzsDMyhghUd4hBb5uxGsFrxzf+WasfcUw9klSfTsRZ09n1BsaaWbgrLjlMQ+EEHS46v5VNo0Ms2CA==
@ -2300,17 +2292,6 @@
dependencies: dependencies:
defer-to-connect "^1.0.1" defer-to-connect "^1.0.1"
"@techpass/passport-openidconnect@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.0.tgz#a60b2bbf3f262649a5a02d5d186219944acc3010"
integrity sha512-bVsPwl66s7J7GHxTPlW/RJYhZol9SshNznQsx83OOh9G+JWFGoeWxh+xbX+FTdJNoUvGIGbJnpWPY2wC6NOHPw==
dependencies:
base64url "^3.0.1"
oauth "^0.9.15"
passport-strategy "^1.0.0"
request "^2.88.0"
webfinger "^0.4.2"
"@tootallnate/once@1": "@tootallnate/once@1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@ -3305,7 +3286,7 @@ base64-js@^1.0.2, base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
base64url@3.x.x, base64url@^3.0.1: base64url@3.x.x:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
@ -8726,7 +8707,7 @@ oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
oauth@0.9.x, oauth@^0.9.15: oauth@0.9.x:
version "0.9.15" version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
@ -10100,7 +10081,7 @@ request-promise-native@^1.0.5:
stealthy-require "^1.1.1" stealthy-require "^1.1.1"
tough-cookie "^2.3.3" tough-cookie "^2.3.3"
"request@>= 2.52.0", request@^2.72.0, request@^2.74.0, request@^2.87.0, request@^2.88.0: "request@>= 2.52.0", request@^2.72.0, request@^2.74.0, request@^2.87.0:
version "2.88.2" version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@ -10224,7 +10205,7 @@ rimraf@2.6.3:
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
rimraf@^3.0.0, rimraf@^3.0.2: rimraf@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
@ -10309,7 +10290,7 @@ sax@1.2.1:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
sax@>=0.1.1, sax@>=0.6.0, sax@^1.2.4: sax@>=0.6.0, sax@^1.2.4:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@ -10764,11 +10745,6 @@ stealthy-require@^1.1.1:
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
step@0.0.x:
version "0.0.6"
resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2"
integrity sha1-FD54SaXX0/SgiP4pr5SRUhbu7eI=
strict-uri-encode@^1.0.0: strict-uri-encode@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@ -11624,6 +11600,11 @@ untildify@^4.0.0:
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
update-dotenv@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/update-dotenv/-/update-dotenv-1.1.1.tgz#17146f302f216c3c92419d5a327a45be910050ca"
integrity sha512-3cIC18In/t0X/yH793c00qqxcKD8jVCgNOPif/fGQkFpYMGecM9YAc+kaAKXuZsM2dE9I9wFI7KvAuNX22SGMQ==
update-notifier@^4.1.0: update-notifier@^4.1.0:
version "4.1.3" version "4.1.3"
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3"
@ -11805,14 +11786,6 @@ walker@^1.0.7, walker@~1.0.5:
dependencies: dependencies:
makeerror "1.0.x" makeerror "1.0.x"
webfinger@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/webfinger/-/webfinger-0.4.2.tgz#3477a6d97799461896039fcffc650b73468ee76d"
integrity sha1-NHem2XeZRhiWA5/P/GULc0aO520=
dependencies:
step "0.0.x"
xml2js "0.1.x"
webidl-conversions@^4.0.2: webidl-conversions@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@ -12019,13 +11992,6 @@ xml-parse-from-string@^1.0.0:
resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
integrity sha1-qQKekp09vN7RafPG4oI42VpdWig= integrity sha1-qQKekp09vN7RafPG4oI42VpdWig=
xml2js@0.1.x:
version "0.1.14"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c"
integrity sha1-UnTmf1pkxfkpdM2FE54DMq3GuQw=
dependencies:
sax ">=0.1.1"
xml2js@0.4.19: xml2js@0.4.19:
version "0.4.19" version "0.4.19"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"

View File

@ -16,7 +16,9 @@
"build:docker": "docker build . -t worker-service", "build:docker": "docker build . -t worker-service",
"dev:stack:init": "node ./scripts/dev/manage.js init", "dev:stack:init": "node ./scripts/dev/manage.js init",
"dev:builder": "npm run dev:stack:init && nodemon src/index.js", "dev:builder": "npm run dev:stack:init && nodemon src/index.js",
"test": "jest --runInBand" "test": "jest --runInBand",
"multi:enable": "node scripts/multiTenancy.js enable",
"multi:disable": "node scripts/multiTenancy.js disable"
}, },
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@ -52,7 +54,8 @@
"jest": "^26.6.3", "jest": "^26.6.3",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"pouchdb-adapter-memory": "^7.2.2", "pouchdb-adapter-memory": "^7.2.2",
"supertest": "^6.1.3" "supertest": "^6.1.3",
"update-dotenv": "^1.1.1"
}, },
"jest": { "jest": {
"testEnvironment": "node", "testEnvironment": "node",

View File

@ -4,26 +4,28 @@ const fs = require("fs")
async function init() { async function init() {
const envFilePath = path.join(process.cwd(), ".env") const envFilePath = path.join(process.cwd(), ".env")
const envFileJson = { if (!fs.existsSync(envFilePath)) {
SELF_HOSTED: 1, const envFileJson = {
PORT: 4002, SELF_HOSTED: 1,
CLUSTER_PORT: 10000, PORT: 4002,
JWT_SECRET: "testsecret", CLUSTER_PORT: 10000,
INTERNAL_API_KEY: "budibase", JWT_SECRET: "testsecret",
MINIO_ACCESS_KEY: "budibase", INTERNAL_API_KEY: "budibase",
MINIO_SECRET_KEY: "budibase", MINIO_ACCESS_KEY: "budibase",
COUCH_DB_USER: "budibase", MINIO_SECRET_KEY: "budibase",
COUCH_DB_PASSWORD: "budibase", REDIS_URL: "localhost:6379",
REDIS_URL: "localhost:6379", REDIS_PASSWORD: "budibase",
REDIS_PASSWORD: "budibase", MINIO_URL: "http://localhost:10000/",
MINIO_URL: "http://localhost:10000/", COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/", // empty string is false
MULTI_TENANCY: "",
}
let envFile = ""
Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n`
})
fs.writeFileSync(envFilePath, envFile)
} }
let envFile = ""
Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n`
})
fs.writeFileSync(envFilePath, envFile)
} }
// if more than init required use this to determine the command type // if more than init required use this to determine the command type

View File

@ -3,3 +3,4 @@ const env = require("../src/environment")
env._set("NODE_ENV", "jest") env._set("NODE_ENV", "jest")
env._set("JWT_SECRET", "test-jwtsecret") env._set("JWT_SECRET", "test-jwtsecret")
env._set("LOG_LEVEL", "silent") env._set("LOG_LEVEL", "silent")
env._set("MULTI_TENANCY", true)

View File

@ -0,0 +1,8 @@
#!/usr/bin/env node
const updateDotEnv = require("update-dotenv")
const arg = process.argv.slice(2)[0]
updateDotEnv({
MULTI_TENANCY: arg === "enable" ? "1" : "",
}).then(() => console.log("Updated worker!"))

View File

@ -1,18 +1,12 @@
const { DocumentTypes } = require("@budibase/auth").db const { getAllApps } = require("@budibase/auth/db")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const APP_PREFIX = "app_"
const URL_REGEX_SLASH = /\/|\\/g const URL_REGEX_SLASH = /\/|\\/g
exports.getApps = async ctx => { exports.getApps = async ctx => {
// allDbs call of CouchDB is very inaccurate in production const tenantId = ctx.user.tenantId
const allDbs = await CouchDB.allDbs() const apps = await getAllApps(CouchDB, { tenantId })
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
const appPromises = appDbNames.map(db =>
new CouchDB(db).get(DocumentTypes.APP_METADATA)
)
const apps = await Promise.allSettled(appPromises)
const body = {} const body = {}
for (let app of apps) { for (let app of apps) {
if (app.status !== "fulfilled") { if (app.status !== "fulfilled") {

View File

@ -2,15 +2,27 @@ const authPkg = require("@budibase/auth")
const { google } = require("@budibase/auth/src/middleware") const { google } = require("@budibase/auth/src/middleware")
const { oidc } = require("@budibase/auth/src/middleware") const { oidc } = require("@budibase/auth/src/middleware")
const { Configs, EmailTemplatePurpose } = require("../../../constants") const { Configs, EmailTemplatePurpose } = require("../../../constants")
const CouchDB = require("../../../db")
const { sendEmail, isEmailConfigured } = require("../../../utilities/email") const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
const { setCookie, getCookie, clearCookie, getGlobalUserByEmail, hash } = const { setCookie, getCookie, clearCookie, getGlobalUserByEmail, hash } =
authPkg.utils authPkg.utils
const { Cookies } = authPkg.constants const { Cookies } = authPkg.constants
const { passport } = authPkg.auth const { passport } = authPkg.auth
const { checkResetPasswordCode } = require("../../../utilities/redis") const { checkResetPasswordCode } = require("../../../utilities/redis")
const {
getGlobalDB,
getTenantId,
isMultiTenant,
} = require("@budibase/auth/tenancy")
const env = require("../../../environment")
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name function googleCallbackUrl() {
let callbackUrl = `/api/global/auth`
if (isMultiTenant()) {
callbackUrl += `/${getTenantId()}`
}
callbackUrl += `/google/callback`
return callbackUrl
}
async function authInternal(ctx, user, err = null, info = null) { async function authInternal(ctx, user, err = null, info = null) {
if (err) { if (err) {
@ -66,6 +78,7 @@ exports.reset = async ctx => {
}) })
} }
} catch (err) { } catch (err) {
console.log(err)
// don't throw any kind of error to the user, this might give away something // don't throw any kind of error to the user, this might give away something
} }
ctx.body = { ctx.body = {
@ -80,7 +93,7 @@ exports.resetUpdate = async ctx => {
const { resetCode, password } = ctx.request.body const { resetCode, password } = ctx.request.body
try { try {
const userId = await checkResetPasswordCode(resetCode) const userId = await checkResetPasswordCode(resetCode)
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const user = await db.get(userId) const user = await db.get(userId)
user.password = await hash(password) user.password = await hash(password)
await db.put(user) await db.put(user)
@ -102,12 +115,14 @@ exports.logout = async ctx => {
* On a successful login, you will be redirected to the googleAuth callback route. * On a successful login, you will be redirected to the googleAuth callback route.
*/ */
exports.googlePreAuth = async (ctx, next) => { exports.googlePreAuth = async (ctx, next) => {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
let callbackUrl = googleCallbackUrl()
const config = await authPkg.db.getScopedConfig(db, { const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE, type: Configs.GOOGLE,
group: ctx.query.group, workspace: ctx.query.workspace,
}) })
const strategy = await google.strategyFactory(config) const strategy = await google.strategyFactory(config, callbackUrl)
return passport.authenticate(strategy, { return passport.authenticate(strategy, {
scope: ["profile", "email"], scope: ["profile", "email"],
@ -115,13 +130,14 @@ exports.googlePreAuth = async (ctx, next) => {
} }
exports.googleAuth = async (ctx, next) => { exports.googleAuth = async (ctx, next) => {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const callbackUrl = googleCallbackUrl()
const config = await authPkg.db.getScopedConfig(db, { const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE, type: Configs.GOOGLE,
group: ctx.query.group, workspace: ctx.query.workspace,
}) })
const strategy = await google.strategyFactory(config) const strategy = await google.strategyFactory(config, callbackUrl)
return passport.authenticate( return passport.authenticate(
strategy, strategy,
@ -135,8 +151,7 @@ exports.googleAuth = async (ctx, next) => {
} }
async function oidcStrategyFactory(ctx, configId) { async function oidcStrategyFactory(ctx, configId) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const config = await authPkg.db.getScopedConfig(db, { const config = await authPkg.db.getScopedConfig(db, {
type: Configs.OIDC, type: Configs.OIDC,
group: ctx.query.group, group: ctx.query.group,
@ -144,9 +159,12 @@ async function oidcStrategyFactory(ctx, configId) {
const chosenConfig = config.configs.filter(c => c.uuid === configId)[0] const chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
// require https callback in production const protocol = env.NODE_ENV === "production" ? "https" : "http"
const protocol = process.env.NODE_ENV === "production" ? "https" : "http" let callbackUrl = `${protocol}://${ctx.host}/api/global/auth`
const callbackUrl = `${protocol}://${ctx.host}/api/admin/auth/oidc/callback` if (isMultiTenant()) {
callbackUrl += `/${getTenantId()}`
}
callbackUrl += `/oidc/callback`
return oidc.strategyFactory(chosenConfig, callbackUrl) return oidc.strategyFactory(chosenConfig, callbackUrl)
} }

View File

@ -1,28 +1,25 @@
const CouchDB = require("../../../db")
const { const {
generateConfigID, generateConfigID,
StaticDatabases,
getConfigParams, getConfigParams,
getGlobalUserParams, getGlobalUserParams,
getScopedFullConfig, getScopedFullConfig,
} = require("@budibase/auth").db getAllApps,
} = require("@budibase/auth/db")
const { Configs } = require("../../../constants") const { Configs } = require("../../../constants")
const email = require("../../../utilities/email") const email = require("../../../utilities/email")
const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore
const CouchDB = require("../../../db")
const APP_PREFIX = "app_" const { getGlobalDB } = require("@budibase/auth/tenancy")
const GLOBAL_DB = StaticDatabases.GLOBAL.name
exports.save = async function (ctx) { exports.save = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const { type, group, user, config } = ctx.request.body const { type, workspace, user, config } = ctx.request.body
// Config does not exist yet // Config does not exist yet
if (!ctx.request.body._id) { if (!ctx.request.body._id) {
ctx.request.body._id = generateConfigID({ ctx.request.body._id = generateConfigID({
type, type,
group, workspace,
user, user,
}) })
} }
@ -51,7 +48,7 @@ exports.save = async function (ctx) {
} }
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const response = await db.allDocs( const response = await db.allDocs(
getConfigParams( getConfigParams(
{ type: ctx.params.type }, { type: ctx.params.type },
@ -65,17 +62,19 @@ exports.fetch = async function (ctx) {
/** /**
* Gets the most granular config for a particular configuration type. * Gets the most granular config for a particular configuration type.
* The hierarchy is type -> group -> user. * The hierarchy is type -> workspace -> user.
*/ */
exports.find = async function (ctx) { exports.find = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const { userId, groupId } = ctx.query const { userId, workspaceId } = ctx.query
if (groupId && userId) { if (workspaceId && userId) {
const group = await db.get(groupId) const workspace = await db.get(workspaceId)
const userInGroup = group.users.some(groupUser => groupUser === userId) const userInWorkspace = workspace.users.some(
if (!ctx.user.admin && !userInGroup) { workspaceUser => workspaceUser === userId
ctx.throw(400, `User is not in specified group: ${group}.`) )
if (!ctx.user.admin && !userInWorkspace) {
ctx.throw(400, `User is not in specified workspace: ${workspace}.`)
} }
} }
@ -84,7 +83,7 @@ exports.find = async function (ctx) {
const scopedConfig = await getScopedFullConfig(db, { const scopedConfig = await getScopedFullConfig(db, {
type: ctx.params.type, type: ctx.params.type,
user: userId, user: userId,
group: groupId, workspace: workspaceId,
}) })
if (scopedConfig) { if (scopedConfig) {
@ -99,7 +98,7 @@ exports.find = async function (ctx) {
} }
exports.publicOidc = async function (ctx) { exports.publicOidc = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
try { try {
// Find the config with the most granular scope based on context // Find the config with the most granular scope based on context
const oidcConfig = await getScopedFullConfig(db, { const oidcConfig = await getScopedFullConfig(db, {
@ -109,14 +108,11 @@ exports.publicOidc = async function (ctx) {
if (!oidcConfig) { if (!oidcConfig) {
ctx.body = {} ctx.body = {}
} else { } else {
const partialOidcCofig = oidcConfig.config.configs.map(config => { ctx.body = oidcConfig.config.configs.map(config => ({
return { logo: config.logo,
logo: config.logo, name: config.name,
name: config.name, uuid: config.uuid,
uuid: config.uuid, }))
}
})
ctx.body = partialOidcCofig
} }
} catch (err) { } catch (err) {
ctx.throw(err.status, err) ctx.throw(err.status, err)
@ -124,7 +120,7 @@ exports.publicOidc = async function (ctx) {
} }
exports.publicSettings = async function (ctx) { exports.publicSettings = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
try { try {
// Find the config with the most granular scope based on context // Find the config with the most granular scope based on context
@ -140,7 +136,7 @@ exports.publicSettings = async function (ctx) {
type: Configs.OIDC, type: Configs.OIDC,
}) })
let config = {} let config
if (!publicConfig) { if (!publicConfig) {
config = { config = {
config: {}, config: {},
@ -151,18 +147,16 @@ exports.publicSettings = async function (ctx) {
// google button flag // google button flag
if (googleConfig && googleConfig.config) { if (googleConfig && googleConfig.config) {
const googleActivated = // activated by default for configs pre-activated flag
googleConfig.config.activated == undefined || // activated by default for configs pre-activated flag config.config.google =
googleConfig.config.activated googleConfig.config.activated == null || googleConfig.config.activated
config.config.google = googleActivated
} else { } else {
config.config.google = false config.config.google = false
} }
// oidc button flag // oidc button flag
if (oidcConfig && oidcConfig.config) { if (oidcConfig && oidcConfig.config) {
const oidcActivated = oidcConfig.config.configs[0].activated config.config.oidc = oidcConfig.config.configs[0].activated
config.config.oidc = oidcActivated
} else { } else {
config.config.oidc = false config.config.oidc = false
} }
@ -191,7 +185,7 @@ exports.upload = async function (ctx) {
// add to configuration structure // add to configuration structure
// TODO: right now this only does a global level // TODO: right now this only does a global level
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
let cfgStructure = await getScopedFullConfig(db, { type }) let cfgStructure = await getScopedFullConfig(db, { type })
if (!cfgStructure) { if (!cfgStructure) {
cfgStructure = { cfgStructure = {
@ -211,7 +205,7 @@ exports.upload = async function (ctx) {
} }
exports.destroy = async function (ctx) { exports.destroy = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const { id, rev } = ctx.params const { id, rev } = ctx.params
try { try {
@ -223,14 +217,13 @@ exports.destroy = async function (ctx) {
} }
exports.configChecklist = async function (ctx) { exports.configChecklist = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
try { try {
// TODO: Watch get started video // TODO: Watch get started video
// Apps exist // Apps exist
let allDbs = await CouchDB.allDbs() const apps = await getAllApps(CouchDB)
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
// They have set up SMTP // They have set up SMTP
const smtpConfig = await getScopedFullConfig(db, { const smtpConfig = await getScopedFullConfig(db, {
@ -246,7 +239,7 @@ exports.configChecklist = async function (ctx) {
const oidcConfig = await getScopedFullConfig(db, { const oidcConfig = await getScopedFullConfig(db, {
type: Configs.OIDC, type: Configs.OIDC,
}) })
// They have set up an admin user // They have set up an global user
const users = await db.allDocs( const users = await db.allDocs(
getGlobalUserParams(null, { getGlobalUserParams(null, {
include_docs: true, include_docs: true,
@ -255,7 +248,7 @@ exports.configChecklist = async function (ctx) {
const adminUser = users.rows.some(row => row.doc.admin) const adminUser = users.rows.some(row => row.doc.admin)
ctx.body = { ctx.body = {
apps: appDbNames.length, apps: apps.length,
smtp: !!smtpConfig, smtp: !!smtpConfig,
adminUser, adminUser,
sso: !!googleConfig || !!oidcConfig, sso: !!googleConfig || !!oidcConfig,

View File

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

View File

@ -7,8 +7,9 @@ const {
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
exports.fetch = async ctx => { exports.fetch = async ctx => {
const tenantId = ctx.user.tenantId
// always use the dev apps as they'll be most up to date (true) // always use the dev apps as they'll be most up to date (true)
const apps = await getAllApps({ CouchDB, all: true }) const apps = await getAllApps(CouchDB, { tenantId, all: true })
const promises = [] const promises = []
for (let app of apps) { for (let app of apps) {
// use dev app IDs // use dev app IDs

View File

@ -1,16 +1,14 @@
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db const { generateTemplateID } = require("@budibase/auth/db")
const CouchDB = require("../../../db")
const { const {
TemplateMetadata, TemplateMetadata,
TemplateBindings, TemplateBindings,
GLOBAL_OWNER, GLOBAL_OWNER,
} = require("../../../constants") } = require("../../../constants")
const { getTemplates } = require("../../../constants/templates") const { getTemplates } = require("../../../constants/templates")
const { getGlobalDB } = require("@budibase/auth/tenancy")
const GLOBAL_DB = StaticDatabases.GLOBAL.name
exports.save = async ctx => { exports.save = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
let template = ctx.request.body let template = ctx.request.body
if (!template.ownerId) { if (!template.ownerId) {
template.ownerId = GLOBAL_OWNER template.ownerId = GLOBAL_OWNER
@ -70,7 +68,7 @@ exports.find = async ctx => {
} }
exports.destroy = async ctx => { exports.destroy = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
await db.remove(ctx.params.id, ctx.params.rev) await db.remove(ctx.params.id, ctx.params.rev)
ctx.message = `Template ${ctx.params.id} deleted.` ctx.message = `Template ${ctx.params.id} deleted.`
ctx.status = 200 ctx.status = 200

View File

@ -1,17 +1,30 @@
const CouchDB = require("../../../db") const {
const { generateGlobalUserID, getGlobalUserParams, StaticDatabases } = generateGlobalUserID,
require("@budibase/auth").db getGlobalUserParams,
StaticDatabases,
} = require("@budibase/auth/db")
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
const { UserStatus, EmailTemplatePurpose } = require("../../../constants") const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
const { DEFAULT_TENANT_ID } = require("@budibase/auth/constants")
const { checkInviteCode } = require("../../../utilities/redis") const { checkInviteCode } = require("../../../utilities/redis")
const { sendEmail } = require("../../../utilities/email") const { sendEmail } = require("../../../utilities/email")
const { user: userCache } = require("@budibase/auth/cache") const { user: userCache } = require("@budibase/auth/cache")
const { invalidateSessions } = require("@budibase/auth/sessions") const { invalidateSessions } = require("@budibase/auth/sessions")
const CouchDB = require("../../../db")
const env = require("../../../environment")
const {
getGlobalDB,
getTenantId,
doesTenantExist,
tryAddTenant,
updateTenantId,
} = require("@budibase/auth/tenancy")
const GLOBAL_DB = StaticDatabases.GLOBAL.name const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
async function allUsers() { async function allUsers() {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const response = await db.allDocs( const response = await db.allDocs(
getGlobalUserParams(null, { getGlobalUserParams(null, {
include_docs: true, include_docs: true,
@ -20,16 +33,21 @@ async function allUsers() {
return response.rows.map(row => row.doc) return response.rows.map(row => row.doc)
} }
exports.save = async ctx => { async function saveUser(user, tenantId) {
const db = new CouchDB(GLOBAL_DB) if (!tenantId) {
const { email, password, _id } = ctx.request.body throw "No tenancy specified."
}
// need to set the context for this request, as specified
updateTenantId(tenantId)
// 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 // make sure another user isn't using the same email
let dbUser let dbUser
if (email) { if (email) {
dbUser = await getGlobalUserByEmail(email) dbUser = await getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
ctx.throw(400, "Email address already in use.") throw "Email address already in use."
} }
} else { } else {
dbUser = await db.get(_id) dbUser = await db.get(_id)
@ -42,14 +60,16 @@ exports.save = async ctx => {
} else if (dbUser) { } else if (dbUser) {
hashedPassword = dbUser.password hashedPassword = dbUser.password
} else { } else {
ctx.throw(400, "Password must be specified.") throw "Password must be specified."
} }
let user = { _id = _id || generateGlobalUserID()
user = {
...dbUser, ...dbUser,
...ctx.request.body, ...user,
_id: _id || generateGlobalUserID(), _id,
password: hashedPassword, password: hashedPassword,
tenantId,
} }
// make sure the roles object is always present // make sure the roles object is always present
if (!user.roles) { if (!user.roles) {
@ -64,23 +84,37 @@ exports.save = async ctx => {
password: hashedPassword, password: hashedPassword,
...user, ...user,
}) })
await tryAddTenant(tenantId, _id, email)
await userCache.invalidateUser(response.id) await userCache.invalidateUser(response.id)
ctx.body = { return {
_id: response.id, _id: response.id,
_rev: response.rev, _rev: response.rev,
email, email,
} }
} catch (err) { } catch (err) {
if (err.status === 409) { if (err.status === 409) {
ctx.throw(400, "User exists already") throw "User exists already"
} else { } else {
ctx.throw(err.status, err) throw err
} }
} }
} }
exports.save = async ctx => {
try {
ctx.body = await saveUser(ctx.request.body, getTenantId())
} catch (err) {
ctx.throw(err.status || 400, err)
}
}
exports.adminUser = async ctx => { exports.adminUser = async ctx => {
const db = new CouchDB(GLOBAL_DB) const { email, password, tenantId } = ctx.request.body
if (await doesTenantExist(tenantId)) {
ctx.throw(403, "Organisation already exists.")
}
const db = getGlobalDB(tenantId)
const response = await db.allDocs( const response = await db.allDocs(
getGlobalUserParams(null, { getGlobalUserParams(null, {
include_docs: true, include_docs: true,
@ -88,11 +122,13 @@ exports.adminUser = async ctx => {
) )
if (response.rows.some(row => row.doc.admin)) { if (response.rows.some(row => row.doc.admin)) {
ctx.throw(403, "You cannot initialise once an admin user has been created.") ctx.throw(
403,
"You cannot initialise once an global user has been created."
)
} }
const { email, password } = ctx.request.body const user = {
ctx.request.body = {
email: email, email: email,
password: password, password: password,
roles: {}, roles: {},
@ -102,12 +138,17 @@ exports.adminUser = async ctx => {
admin: { admin: {
global: true, global: true,
}, },
tenantId,
}
try {
ctx.body = await saveUser(user, tenantId)
} catch (err) {
ctx.throw(err.status || 400, err)
} }
await exports.save(ctx)
} }
exports.destroy = async ctx => { exports.destroy = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const dbUser = await db.get(ctx.params.id) const dbUser = await db.get(ctx.params.id)
await db.remove(dbUser._id, dbUser._rev) await db.remove(dbUser._id, dbUser._rev)
await userCache.invalidateUser(dbUser._id) await userCache.invalidateUser(dbUser._id)
@ -119,8 +160,8 @@ exports.destroy = async ctx => {
exports.removeAppRole = async ctx => { exports.removeAppRole = async ctx => {
const { appId } = ctx.params const { appId } = ctx.params
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const users = await allUsers() const users = await allUsers(ctx)
const bulk = [] const bulk = []
const cacheInvalidations = [] const cacheInvalidations = []
for (let user of users) { for (let user of users) {
@ -149,7 +190,7 @@ exports.getSelf = async ctx => {
} }
exports.updateSelf = async ctx => { exports.updateSelf = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const user = await db.get(ctx.user._id) const user = await db.get(ctx.user._id)
if (ctx.request.body.password) { if (ctx.request.body.password) {
ctx.request.body.password = await hash(ctx.request.body.password) ctx.request.body.password = await hash(ctx.request.body.password)
@ -170,7 +211,7 @@ exports.updateSelf = async ctx => {
// called internally by app server user fetch // called internally by app server user fetch
exports.fetch = async ctx => { exports.fetch = async ctx => {
const users = await allUsers() const users = await allUsers(ctx)
// user hashed password shouldn't ever be returned // user hashed password shouldn't ever be returned
for (let user of users) { for (let user of users) {
if (user) { if (user) {
@ -182,7 +223,7 @@ exports.fetch = async ctx => {
// called internally by app server user find // called internally by app server user find
exports.find = async ctx => { exports.find = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
let user let user
try { try {
user = await db.get(ctx.params.id) user = await db.get(ctx.params.id)
@ -196,12 +237,38 @@ exports.find = async ctx => {
ctx.body = user ctx.body = user
} }
exports.tenantLookup = async ctx => {
const id = ctx.params.id
// lookup, could be email or userId, either will return a doc
const db = new CouchDB(PLATFORM_INFO_DB)
let tenantId = null
try {
const doc = await db.get(id)
if (doc && doc.tenantId) {
tenantId = doc.tenantId
}
} catch (err) {
if (!env.MULTI_TENANCY) {
tenantId = DEFAULT_TENANT_ID
} else {
ctx.throw(400, "No tenant found.")
}
}
ctx.body = {
tenantId,
}
}
exports.invite = async ctx => { exports.invite = async ctx => {
const { email, userInfo } = ctx.request.body let { email, userInfo } = ctx.request.body
const existing = await getGlobalUserByEmail(email) const existing = await getGlobalUserByEmail(email)
if (existing) { if (existing) {
ctx.throw(400, "Email address already in use.") ctx.throw(400, "Email address already in use.")
} }
if (!userInfo) {
userInfo = {}
}
userInfo.tenantId = getTenantId()
await sendEmail(email, EmailTemplatePurpose.INVITATION, { await sendEmail(email, EmailTemplatePurpose.INVITATION, {
subject: "{{ company }} platform invitation", subject: "{{ company }} platform invitation",
info: userInfo, info: userInfo,
@ -214,18 +281,18 @@ exports.invite = async ctx => {
exports.inviteAccept = async ctx => { exports.inviteAccept = async ctx => {
const { inviteCode, password, firstName, lastName } = ctx.request.body const { inviteCode, password, firstName, lastName } = ctx.request.body
try { try {
// info is an extension of the user object that was stored by admin // info is an extension of the user object that was stored by global
const { email, info } = await checkInviteCode(inviteCode) const { email, info } = await checkInviteCode(inviteCode)
// only pass through certain props for accepting ctx.body = await saveUser(
ctx.request.body = { {
firstName, firstName,
lastName, lastName,
password, password,
email, email,
...info, ...info,
} },
// this will flesh out the body response info.tenantId
await exports.save(ctx) )
} catch (err) { } catch (err) {
ctx.throw(400, "Unable to create new user, invitation invalid.") ctx.throw(400, "Unable to create new user, invitation invalid.")
} }

View File

@ -1,20 +1,17 @@
const CouchDB = require("../../../db") const { getWorkspaceParams, generateWorkspaceID } = require("@budibase/auth/db")
const { getGroupParams, generateGroupID, StaticDatabases } = const { getGlobalDB } = require("@budibase/auth/tenancy")
require("@budibase/auth").db
const GLOBAL_DB = StaticDatabases.GLOBAL.name
exports.save = async function (ctx) { exports.save = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const groupDoc = ctx.request.body const workspaceDoc = ctx.request.body
// Group does not exist yet // workspace does not exist yet
if (!groupDoc._id) { if (!workspaceDoc._id) {
groupDoc._id = generateGroupID() workspaceDoc._id = generateWorkspaceID()
} }
try { try {
const response = await db.post(groupDoc) const response = await db.post(workspaceDoc)
ctx.body = { ctx.body = {
_id: response.id, _id: response.id,
_rev: response.rev, _rev: response.rev,
@ -25,9 +22,9 @@ exports.save = async function (ctx) {
} }
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const response = await db.allDocs( const response = await db.allDocs(
getGroupParams(undefined, { getWorkspaceParams(undefined, {
include_docs: true, include_docs: true,
}) })
) )
@ -35,7 +32,7 @@ exports.fetch = async function (ctx) {
} }
exports.find = async function (ctx) { exports.find = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
try { try {
ctx.body = await db.get(ctx.params.id) ctx.body = await db.get(ctx.params.id)
} catch (err) { } catch (err) {
@ -44,12 +41,12 @@ exports.find = async function (ctx) {
} }
exports.destroy = async function (ctx) { exports.destroy = async function (ctx) {
const db = new CouchDB(GLOBAL_DB) const db = getGlobalDB()
const { id, rev } = ctx.params const { id, rev } = ctx.params
try { try {
await db.remove(id, rev) await db.remove(id, rev)
ctx.body = { message: "Group deleted successfully" } ctx.body = { message: "Workspace deleted successfully" }
} catch (err) { } catch (err) {
ctx.throw(err.status, err) ctx.throw(err.status, err)
} }

View File

@ -0,0 +1,7 @@
const env = require("../../../environment")
exports.fetch = async ctx => {
ctx.body = {
multiTenancy: !!env.MULTI_TENANCY,
}
}

View File

@ -0,0 +1,33 @@
const CouchDB = require("../../../db")
const { StaticDatabases } = require("@budibase/auth/db")
exports.exists = async ctx => {
const tenantId = ctx.request.params
const db = new CouchDB(StaticDatabases.PLATFORM_INFO.name)
let exists = false
try {
const tenantsDoc = await db.get(StaticDatabases.PLATFORM_INFO.docs.tenants)
if (tenantsDoc) {
exists = tenantsDoc.tenantIds.indexOf(tenantId) !== -1
}
} catch (err) {
// if error it doesn't exist
}
ctx.body = {
exists,
}
}
exports.fetch = async ctx => {
const db = new CouchDB(StaticDatabases.PLATFORM_INFO.name)
let tenants = []
try {
const tenantsDoc = await db.get(StaticDatabases.PLATFORM_INFO.docs.tenants)
if (tenantsDoc) {
tenants = tenantsDoc.tenantIds
}
} catch (err) {
// if error it doesn't exist
}
ctx.body = tenants
}

View File

@ -2,55 +2,51 @@ const Router = require("@koa/router")
const compress = require("koa-compress") const compress = require("koa-compress")
const zlib = require("zlib") const zlib = require("zlib")
const { routes } = require("./routes") const { routes } = require("./routes")
const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } =
require("@budibase/auth").auth
const PUBLIC_ENDPOINTS = [ const PUBLIC_ENDPOINTS = [
{ {
route: "/api/admin/users/init", // this covers all of the POST auth routes
route: "/api/global/auth/:tenantId",
method: "POST", method: "POST",
}, },
{ {
route: "/api/admin/users/invite/accept", // this covers all of the GET auth routes
route: "/api/global/auth/:tenantId",
method: "GET",
},
{
// this covers all of the public config routes
route: "/api/global/configs/public",
method: "GET",
},
{
route: "/api/global/configs/checklist",
method: "GET",
},
{
route: "/api/global/users/init",
method: "POST", method: "POST",
}, },
{ {
route: "/api/admin/auth", route: "/api/global/users/invite/accept",
method: "POST", method: "POST",
}, },
{ {
route: "/api/admin/auth/google", route: "api/system/flags",
method: "GET", method: "GET",
}, },
]
const NO_TENANCY_ENDPOINTS = [
...PUBLIC_ENDPOINTS,
{ {
route: "/api/admin/auth/google/callback", route: "/api/system",
method: "GET", method: "ALL",
}, },
{ {
route: "/api/admin/auth/oidc", route: "/api/global/users/self",
method: "GET",
},
{
route: "/api/admin/auth/oidc/callback",
method: "GET",
},
{
route: "/api/admin/auth/reset",
method: "POST",
},
{
route: "/api/admin/configs/checklist",
method: "GET",
},
{
route: "/api/apps",
method: "GET",
},
{
route: "/api/admin/configs/public",
method: "GET",
},
{
route: "/api/admin/configs/publicOidc",
method: "GET", method: "GET",
}, },
] ]
@ -71,9 +67,10 @@ router
) )
.use("/health", ctx => (ctx.status = 200)) .use("/health", ctx => (ctx.status = 200))
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS)) .use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
// for now no public access is allowed to worker (bar health check) // for now no public access is allowed to worker (bar health check)
.use((ctx, next) => { .use((ctx, next) => {
if (!ctx.isAuthenticated) { if (!ctx.isAuthenticated && !ctx.publicEndpoint) {
ctx.throw(403, "Unauthorized - no public worker access") ctx.throw(403, "Unauthorized - no public worker access")
} }
return next() return next()

View File

@ -1,45 +0,0 @@
const Router = require("@koa/router")
const authController = require("../../controllers/admin/auth")
const joiValidator = require("../../../middleware/joi-validator")
const Joi = require("joi")
const router = Router()
function buildAuthValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}).required().unknown(false))
}
function buildResetValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
email: Joi.string().required(),
}).required().unknown(false))
}
function buildResetUpdateValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
resetCode: Joi.string().required(),
password: Joi.string().required(),
}).required().unknown(false))
}
router
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate)
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset)
.post(
"/api/admin/auth/reset/update",
buildResetUpdateValidation(),
authController.resetUpdate
)
.post("/api/admin/auth/logout", authController.logout)
.get("/api/admin/auth/google", authController.googlePreAuth)
.get("/api/admin/auth/google/callback", authController.googleAuth)
.get("/api/admin/auth/oidc/configs/:configId", authController.oidcPreAuth)
.get("/api/admin/auth/oidc/callback", authController.oidcAuth)
module.exports = router

View File

@ -1,11 +0,0 @@
const Router = require("@koa/router")
const controller = require("../../controllers/admin/roles")
const adminOnly = require("../../../middleware/adminOnly")
const router = Router()
router
.get("/api/admin/roles", adminOnly, controller.fetch)
.get("/api/admin/roles/:appId", adminOnly, controller.find)
module.exports = router

Some files were not shown because too many files have changed in this diff Show More