Revert "Multi-tenancy/organisations"

This commit is contained in:
Michael Drury 2021-08-04 10:02:24 +01:00 committed by GitHub
parent 2456e69483
commit b86a6fddc9
127 changed files with 881 additions and 2359 deletions

View File

@ -26,18 +26,10 @@ 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,19 +37,11 @@ static_resources:
route: route:
cluster: app-service cluster: app-service
# special cases for worker admin (deprecated), global and system API # special case for worker admin 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,8 +43,6 @@
"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,4 +1 @@
module.exports = { module.exports = require("./src/db/utils")
...require("./src/db/utils"),
...require("./src/db/constants"),
}

View File

@ -13,7 +13,6 @@
"@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,21 +1,15 @@
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, tenantId = null) => { exports.getUser = async userId => {
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 getGlobalDB(tenantId).get(userId) user = await getDB(StaticDatabases.GLOBAL.name).get(userId)
client.store(userId, user, EXPIRY_SECONDS) client.store(userId, user, EXPIRY_SECONDS)
} }
return user return user

View File

@ -14,14 +14,13 @@ 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",
WORKSPACE_MANAGER: "workspace_manager", GROUP_MANAGER: "group_manager",
} }
exports.Configs = { exports.Configs = {
@ -32,5 +31,3 @@ exports.Configs = {
OIDC: "oidc", OIDC: "oidc",
OIDC_LOGOS: "logos_oidc", OIDC_LOGOS: "logos_oidc",
} }
exports.DEFAULT_TENANT_ID = "default"

View File

@ -1,17 +0,0 @@
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 = StaticDatabases exports.StaticDatabases = {
GLOBAL: {
const PRE_APP = "app" name: "global-db",
const PRE_DEV = "dev" },
DEPLOYMENTS: {
name: "deployments",
},
}
const DocumentTypes = { const DocumentTypes = {
USER: "us", USER: "us",
WORKSPACE: "workspace", GROUP: "group",
CONFIG: "config", CONFIG: "config",
TEMPLATE: "template", TEMPLATE: "template",
APP: PRE_APP, APP: "app",
DEV: PRE_DEV, APP_DEV: "app_dev",
APP_DEV: `${PRE_APP}${SEPARATOR}${PRE_DEV}`, APP_METADATA: "app_metadata",
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 = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR 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 workspace ID. * Generates a new group ID.
* @returns {string} The new workspace ID which the workspace doc can be stored under. * @returns {string} The new group ID which the group doc can be stored under.
*/ */
exports.generateWorkspaceID = () => { exports.generateGroupID = () => {
return `${DocumentTypes.WORKSPACE}${SEPARATOR}${newid()}` return `${DocumentTypes.GROUP}${SEPARATOR}${newid()}`
} }
/** /**
* Gets parameters for retrieving workspaces. * Gets parameters for retrieving groups.
*/ */
exports.getWorkspaceParams = (id = "", otherProps = {}) => { exports.getGroupParams = (id = "", otherProps = {}) => {
return { return {
...otherProps, ...otherProps,
startkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}`, startkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}`,
endkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`, endkey: `${DocumentTypes.GROUP}${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 workspace level. * @param ownerId The owner/user of the template, this could be global or a group 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 workspace level. * Gets parameters for retrieving templates. Owner ID must be specified, either global or a group level.
*/ */
exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => { exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
if (!templateId) { if (!templateId) {
@ -163,26 +163,11 @@ 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 =>
const split = dbName.split(SEPARATOR) dbName.startsWith(exports.APP_PREFIX)
// 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)
@ -229,8 +214,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, workspace, user }) => { const generateConfigID = ({ type, group, user }) => {
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}` return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
} }
@ -238,8 +223,8 @@ const generateConfigID = ({ type, workspace, user }) => {
/** /**
* Gets parameters for retrieving configurations. * Gets parameters for retrieving configurations.
*/ */
const getConfigParams = ({ type, workspace, user }, otherProps = {}) => { const getConfigParams = ({ type, group, user }, otherProps = {}) => {
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
return { return {
...otherProps, ...otherProps,
@ -249,15 +234,15 @@ const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
} }
/** /**
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed. * Returns the most granular configuration document from the DB based on the type, group and userID passed.
* @param {Object} db - db instance to query * @param {Object} db - db instance to query
* @param {Object} scopes - the type, workspace and userID scopes of the configuration. * @param {Object} scopes - the type, group 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, workspace }) { const getScopedFullConfig = async function (db, { type, user, group }) {
const response = await db.allDocs( const response = await db.allDocs(
getConfigParams( getConfigParams(
{ type, user, workspace }, { type, user, group },
{ {
include_docs: true, include_docs: true,
} }
@ -267,14 +252,14 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) {
function determineScore(row) { function determineScore(row) {
const config = row.doc const config = row.doc
// Config is specific to a user and a workspace // Config is specific to a user and a group
if (config._id.includes(generateConfigID({ type, user, workspace }))) { if (config._id.includes(generateConfigID({ type, user, group }))) {
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, workspace }))) { } else if (config._id.includes(generateConfigID({ type, group }))) {
// Config is specific to a workspace only // Config is specific to a group 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,4 +1,5 @@
const { DocumentTypes, ViewNames } = require("./utils") const { DocumentTypes, ViewNames, StaticDatabases } = require("./utils")
const { getDB } = require("./index")
function DesignDoc() { function DesignDoc() {
return { return {
@ -9,7 +10,8 @@ function DesignDoc() {
} }
} }
exports.createUserEmailView = async db => { exports.createUserEmailView = async () => {
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,7 +16,6 @@ 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,7 +2,6 @@ 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,
@ -10,9 +9,8 @@ const {
google, google,
oidc, oidc,
auditLog, auditLog,
tenancy,
} = require("./middleware") } = require("./middleware")
const { setDB } = require("./db") const { setDB, getDB } = require("./db")
const userCache = require("./cache/user") const userCache = require("./cache/user")
// Strategies // Strategies
@ -22,7 +20,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 = getGlobalDB() const db = getDB(StaticDatabases.GLOBAL.name)
try { try {
const user = await db.get(user._id) const user = await db.get(user._id)
@ -56,7 +54,6 @@ module.exports = {
google, google,
oidc, oidc,
jwt: require("jsonwebtoken"), jwt: require("jsonwebtoken"),
buildTenancyMiddleware: tenancy,
auditLog, auditLog,
}, },
cache: { cache: {

View File

@ -2,34 +2,46 @@ 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")
function finalise( const PARAM_REGEX = /\/:(.*?)\//g
ctx,
{ authenticated, user, internal, version, publicEndpoint } = {} function buildNoAuthRegex(patterns) {
) { return patterns.map(pattern => {
ctx.publicEndpoint = publicEndpoint || false 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) {
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) => {
* This middleware is tenancy aware, so that it does not depend on other middlewares being used. const noAuthOptions = noAuthPatterns ? buildNoAuthRegex(noAuthPatterns) : []
* 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 = matches(ctx, noAuthOptions) const found = noAuthOptions.find(({ regex, method }) => {
if (found) { return (
publicEndpoint = true regex.test(ctx.request.url) &&
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
@ -46,7 +58,7 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
error = "No session found" error = "No session found"
} else { } else {
try { try {
user = await getUser(userId, session.tenantId) user = await getUser(userId)
delete user.password delete user.password
authenticated = true authenticated = true
} catch (err) { } catch (err) {
@ -62,26 +74,22 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
} }
} }
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, publicEndpoint }) finalise(ctx, { authenticated, user, internal, version })
return next() return next()
} catch (err) { } catch (err) {
// allow configuring for public access // allow configuring for public access
if ((opts && opts.publicAllowed) || publicEndpoint) { if (opts && opts.publicAllowed) {
finalise(ctx, { authenticated: false, version, publicEndpoint }) finalise(ctx, { authenticated: false, version })
} else { } else {
ctx.throw(err.status || 403, err) ctx.throw(err.status || 403, err)
} }

View File

@ -4,7 +4,6 @@ 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,
@ -13,5 +12,4 @@ module.exports = {
local, local,
authenticated, authenticated,
auditLog, auditLog,
tenancy,
} }

View File

@ -1,33 +0,0 @@
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, callbackUrl) { exports.strategyFactory = async function (config) {
try { try {
const { clientID, clientSecret } = config const { clientID, clientSecret, callbackURL } = config
if (!clientID || !clientSecret) { if (!clientID || !clientSecret || !callbackURL) {
throw new Error( throw new Error(
"Configuration invalid. Must contain google clientID and clientSecret" "Configuration invalid. Must contain google clientID, clientSecret and callbackURL"
) )
} }
@ -41,7 +41,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
{ {
clientID: config.clientID, clientID: config.clientID,
clientSecret: config.clientSecret, clientSecret: config.clientSecret,
callbackURL: callbackUrl, callbackURL: config.callbackURL,
}, },
authenticate authenticate
) )

View File

@ -6,23 +6,19 @@ 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 {*} ctx the request structure * @param {*} email - username to login with
* @param {*} email username to login with * @param {*} password - plain text password to log in with
* @param {*} password plain text password to log in with * @param {*} done - callback from passport to return user information and errors
* @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 (ctx, email, password, done) { exports.authenticate = async function (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")
@ -39,14 +35,12 @@ exports.authenticate = async function (ctx, email, password, done) {
// authenticate // authenticate
if (await compare(password, dbUser.password)) { if (await compare(password, dbUser.password)) {
const sessionId = newid() const sessionId = newid()
const tenantId = getTenantId() await createASession(dbUser._id, sessionId)
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,9 +2,8 @@
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,
} }
@ -28,13 +27,12 @@ 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")
const callbackUrl = `/api/global/auth/${TENANT_ID}/google/callback` await google.strategyFactory(googleConfig)
await google.strategyFactory(googleConfig, callbackUrl)
const expectedOptions = { const expectedOptions = {
clientID: googleConfig.clientID, clientID: googleConfig.clientID,
clientSecret: googleConfig.clientSecret, clientSecret: googleConfig.clientSecret,
callbackURL: callbackUrl, callbackURL: googleConfig.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 { generateGlobalUserID } = require("../../db/utils") const database = require("../../db")
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,21 +15,19 @@ 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 {
@ -75,8 +73,7 @@ exports.authenticateThirdParty = async function (
// authenticate // authenticate
const sessionId = newid() const sessionId = newid()
const tenantId = getTenantId() await createASession(dbUser._id, sessionId)
await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign( dbUser.token = jwt.sign(
{ {

View File

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

View File

@ -1,73 +0,0 @@
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

@ -1,81 +0,0 @@
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

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

View File

@ -1,105 +0,0 @@
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,9 +1,14 @@
const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils") const {
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
@ -106,7 +111,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 = getGlobalDB() const db = getDB(StaticDatabases.GLOBAL.name)
try { try {
let users = ( let users = (
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
@ -118,7 +123,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(db) await createUserEmailView()
return exports.getGlobalUserByEmail(email) return exports.getGlobalUserByEmail(email)
} else { } else {
throw err throw err

View File

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

View File

@ -798,13 +798,6 @@ 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"
@ -1151,15 +1144,6 @@ 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"
@ -1460,13 +1444,6 @@ 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"
@ -4058,7 +4035,7 @@ saxes@^5.0.1:
dependencies: dependencies:
xmlchars "^2.2.0" xmlchars "^2.2.0"
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: "semver@2 || 3 || 4 || 5", 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==
@ -4119,11 +4096,6 @@ 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"
@ -4278,11 +4250,6 @@ 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,10 +4,7 @@
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()
@ -15,14 +12,9 @@
loaded = true loaded = true
}) })
$: {
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 // Force creation of an admin user if one doesn't exist
else if (loaded && apiReady && !hasAdminUser) { $: {
if (loaded && !hasAdminUser) {
$redirect("./admin") $redirect("./admin")
} }
} }
@ -37,7 +29,7 @@
!$isActive("./invite") !$isActive("./invite")
) { ) {
const returnUrl = encodeURIComponent(window.location.pathname) const returnUrl = encodeURIComponent(window.location.pathname)
$redirect("./auth?", { returnUrl }) $redirect("./auth/login?", { returnUrl })
} else if ($auth?.user?.forceResetPassword) { } else if ($auth?.user?.forceResetPassword) {
$redirect("./auth/reset") $redirect("./auth/reset")
} }

View File

@ -6,25 +6,20 @@
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, auth } from "stores/portal" import { admin } 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/global/users/init`, adminUser) const response = await api.post(`/api/admin/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)
@ -52,22 +47,9 @@
<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>
<Layout gap="XS" noPadding>
<Button cta disabled={error} on:click={save}> <Button cta disabled={error} on:click={save}>
Create super admin user Create super admin user
</Button> </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,18 +1,14 @@
<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 { auth, organisation } from "stores/portal" import { 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={() => on:click={() => window.open("/api/admin/auth/google", "_blank")}
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/global/auth/oidc/configs/${$oidc.uuid}`, "_blank")} window.open(`/api/admin/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,12 +6,10 @@
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 = ""
@ -43,12 +41,9 @@
</Body> </Body>
<Input label="Email" bind:value={email} /> <Input label="Email" bind:value={email} />
</Layout> </Layout>
<Layout gap="XS" nopadding>
<Button cta on:click={forgot} disabled={!email}> <Button cta on:click={forgot} disabled={!email}>
Reset your password Reset your password
</Button> </Button>
<ActionButton quiet on:click={() => $goto("../")}>Back</ActionButton>
</Layout>
</Layout> </Layout>
</div> </div>
</div> </div>

View File

@ -1,24 +1,4 @@
<script> <script>
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
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") $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, admin } from "stores/portal" import { auth, organisation, oidc } 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,10 +18,8 @@
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 {
@ -29,6 +27,7 @@
username, username,
password, password,
}) })
notifications.success("Logged in successfully")
if ($auth?.user?.forceResetPassword) { if ($auth?.user?.forceResetPassword) {
$goto("./reset") $goto("./reset")
} else { } else {
@ -51,7 +50,6 @@
onMount(async () => { onMount(async () => {
await organisation.init() await organisation.init()
loaded = true
}) })
</script> </script>
@ -63,10 +61,8 @@
<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>
{#if loaded}
<GoogleButton /> <GoogleButton />
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} /> <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>
@ -83,17 +79,6 @@
<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

@ -1,73 +0,0 @@
<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,15 +2,13 @@
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`) $redirect("./auth/login")
} 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,6 +7,7 @@
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,
@ -21,51 +22,36 @@
} 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, auth, admin } from "stores/portal" import { organisation } 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",
} }
function callbackUrl(tenantId, end) { const GoogleConfigFields = {
let url = `/api/global/auth` Google: ["clientID", "clientSecret", "callbackURL"],
if (multiTenancyEnabled && tenantId) {
url += `/${tenantId}`
} }
url += end const GoogleConfigLabels = {
return url Google: {
} clientID: "Client ID",
clientSecret: "Client secret",
$: GoogleConfigFields = { callbackURL: "Callback URL",
Google: [
{ name: "clientID", label: "Client ID" },
{ name: "clientSecret", label: "Client secret" },
{
name: "callbackURL",
label: "Callback URL",
readonly: true,
placeholder: callbackUrl(tenantId, "/google/callback"),
}, },
],
} }
$: OIDCConfigFields = { const OIDCConfigFields = {
Oidc: [ Oidc: ["configUrl", "clientID", "clientSecret"],
{ name: "configUrl", label: "Config URL" }, }
{ name: "clientID", label: "Client ID" }, const OIDCConfigLabels = {
{ name: "clientSecret", label: "Client Secret" }, Oidc: {
{ configUrl: "Config URL",
name: "callbackURL", clientID: "Client ID",
label: "Callback URL", clientSecret: "Client Secret",
readonly: true,
placeholder: callbackUrl(tenantId, "/oidc/callback"),
}, },
],
} }
let iconDropdownOptions = [ let iconDropdownOptions = [
@ -123,13 +109,17 @@
// 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?.clientSecret providers.google?.config?.clientID ||
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?.clientSecret providers.google?.config?.clientID &&
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 &&
@ -139,7 +129,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/global/configs/upload/logos_oidc/${file.name}`, `/api/admin/configs/upload/logos_oidc/${file.name}`,
data, data,
{} {}
) )
@ -159,21 +149,17 @@
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.
for (let config of element.config.configs) { element.config.configs.forEach(config => {
if (!config.uuid) { !config.uuid && (config.uuid = 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/global/configs`, element)) calls.push(api.post(`/api/admin/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)
@ -187,8 +173,7 @@
`Please fill in all required ${ConfigTypes.Google} fields` `Please fill in all required ${ConfigTypes.Google} fields`
) )
} else { } else {
delete element.config.callbackURL calls.push(api.post(`/api/admin/configs`, element))
calls.push(api.post(`/api/global/configs`, element))
googleSaveButtonDisabled = true googleSaveButtonDisabled = true
originalGoogleDoc = cloneDeep(providers.google) originalGoogleDoc = cloneDeep(providers.google)
} }
@ -221,7 +206,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/global/configs/${ConfigTypes.Google}` `/api/admin/configs/${ConfigTypes.Google}`
) )
const googleDoc = await googleResponse.json() const googleDoc = await googleResponse.json()
@ -242,7 +227,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/global/configs/logos_oidc`) const res = await api.get(`/api/admin/configs/logos_oidc`)
const configSettings = await res.json() const configSettings = await res.json()
if (configSettings.config) { if (configSettings.config) {
@ -257,16 +242,17 @@
}) })
}) })
} }
const oidcResponse = await api.get( const oidcResponse = await api.get(`/api/admin/configs/${ConfigTypes.OIDC}`)
`/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
} }
@ -309,12 +295,8 @@
<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">{field.label}</Label> <Label size="L">{GoogleConfigLabels.Google[field]}</Label>
<Input <Input bind:value={providers.google.config[field]} />
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">
@ -353,14 +335,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">{field.label}</Label> <Label size="L">{OIDCConfigLabels.Oidc[field]}</Label>
<Input <Input bind:value={providers.oidc.config.configs[0][field]} />
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/global/configs`, smtp) const response = await api.post(`/api/admin/configs`, smtp)
if (response.status !== 200) { if (response.status !== 200) {
const error = await response.text() const error = await response.text()
@ -75,9 +75,7 @@
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( const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
`/api/global/configs/${ConfigTypes.SMTP}`
)
const smtpDoc = await smtpResponse.json() const smtpDoc = await smtpResponse.json()
if (!smtpDoc._id) { if (!smtpDoc._id) {
@ -94,15 +92,10 @@
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
if (!smtpDoc.config) {
smtpDoc.config = {}
}
if (!smtpDoc.config.auth) {
smtpConfig.config.auth = { smtpConfig.config.auth = {
type: "login", type: "login",
} }
} }
}
fetchSmtp() fetchSmtp()
</script> </script>

View File

@ -47,8 +47,8 @@
}) })
let selectedApp let selectedApp
const userFetch = fetchData(`/api/global/users/${userId}`) const userFetch = fetchData(`/api/admin/users/${userId}`)
const apps = fetchData(`/api/global/roles`) const apps = fetchData(`/api/admin/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/global/configs/upload/settings/logo", data, {}) const res = await post("/api/admin/configs/upload/settings/logo", data, {})
return await res.json() return await res.json()
} }

View File

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

View File

@ -1,18 +1,12 @@
import { writable, get } from "svelte/store" import { writable } 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 admin = writable({ const { subscribe, set } = writable({})
loaded: false,
})
async function init() { async function init() {
try { try {
const tenantId = get(auth).tenantId const response = await api.get("/api/admin/configs/checklist")
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)
@ -22,49 +16,20 @@ export function createAdminStore() {
0 0
) )
await multiTenancyEnabled() set({
admin.update(store => { checklist: json,
store.loaded = true onboardingProgress: (stepsComplete / onboardingSteps.length) * 100,
store.checklist = json
store.onboardingProgress =
(stepsComplete / onboardingSteps.length) * 100
return store
}) })
} catch (err) { } catch (err) {
admin.update(store => { set({
store.checklist = null 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: admin.subscribe, subscribe,
init, init,
unload,
} }
} }

View File

@ -1,124 +1,74 @@
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 auth = writable({ const user = writable(null)
user: null, const store = derived(user, $user => {
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 ($store.user) { if ($user) {
const user = $store.user if ($user.firstName) {
if (user.firstName) { initials = $user.firstName[0]
initials = user.firstName[0] if ($user.lastName) {
if (user.lastName) { initials += $user.lastName[0]
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: $store.user, user: $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/global/users/self") const response = await api.get("/api/admin/users/self")
if (response.status !== 200) { if (response.status !== 200) {
setUser(null) user.set(null)
} else { } else {
const json = await response.json() const json = await response.json()
setUser(json) user.set(json)
} }
}, },
login: async creds => { login: async creds => {
const tenantId = get(store).tenantId const response = await api.post(`/api/admin/auth`, creds)
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) {
setUser(json.user) user.set(json.user)
} else { } else {
throw "Invalid credentials" throw "Invalid credentials"
} }
return json return json
}, },
logout: async () => { logout: async () => {
const response = await api.post(`/api/global/auth/logout`) const response = await api.post(`/api/admin/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()
setUser(null) user.set(null)
}, },
updateSelf: async fields => { updateSelf: async fields => {
const newUser = { ...get(auth).user, ...fields } const newUser = { ...get(user), ...fields }
const response = await api.post("/api/global/users/self", newUser) const response = await api.post("/api/admin/users/self", newUser)
if (response.status === 200) { if (response.status === 200) {
setUser(newUser) user.set(newUser)
} else { } else {
throw "Unable to update user details" throw "Unable to update user details"
} }
}, },
forgotPassword: async email => { forgotPassword: async email => {
const tenantId = get(store).tenantId const response = await api.post(`/api/admin/auth/reset`, {
const response = await api.post(`/api/global/auth/${tenantId}/reset`, {
email, email,
}) })
if (response.status !== 200) { if (response.status !== 200) {
@ -127,21 +77,17 @@ export function createAuthStore() {
await response.json() await response.json()
}, },
resetPassword: async (password, code) => { resetPassword: async (password, code) => {
const tenantId = get(store).tenantId const response = await api.post(`/api/admin/auth/reset/update`, {
const response = await api.post(
`/api/global/auth/${tenantId}/reset/update`,
{
password, password,
resetCode: code, 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/global/users`, user) const response = await api.post(`/api/admin/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/global/template/definitions`) const response = await api.get(`/api/admin/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/global/template/email`) const templatesResponse = await api.get(`/api/admin/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/global/template`, template) const response = await api.post(`/api/admin/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,6 +1,5 @@
import { writable, get } from "svelte/store" import { writable } 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,
@ -13,13 +12,10 @@ export function createOidcStore() {
const { set, subscribe } = store const { set, subscribe } = store
async function init() { async function init() {
const tenantId = get(auth).tenantId const res = await api.get(`/api/admin/configs/publicOidc`)
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 || Object.keys(json).length === 0) { if (json.status === 400) {
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,9 +1,8 @@
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:10000", platformUrl: "http://localhost:1000",
logoUrl: undefined, logoUrl: undefined,
docsUrl: undefined, docsUrl: undefined,
company: "Budibase", company: "Budibase",
@ -16,8 +15,7 @@ export function createOrganisationStore() {
const { subscribe, set } = store const { subscribe, set } = store
async function init() { async function init() {
const tenantId = get(auth).tenantId const res = await api.get(`/api/admin/configs/public`)
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) {
@ -28,7 +26,7 @@ export function createOrganisationStore() {
} }
async function save(config) { async function save(config) {
const res = await api.post("/api/global/configs", { const res = await api.post("/api/admin/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/global/users`) const response = await api.get(`/api/admin/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/global/users/invite`, body) const response = await api.post(`/api/admin/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/global/users/invite/accept", { const response = await api.post("/api/admin/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/global/users", body) const response = await api.post("/api/admin/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/global/users/${id}`) const response = await api.delete(`/api/admin/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/global/users`, data) const res = await post(`/api/admin/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/global/auth", url: "/api/admin/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 && user._id) { if (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/global")) { if (url.includes("/api/admin")) {
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,9 +23,7 @@
"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",
@ -138,8 +136,7 @@
"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,7 +33,6 @@ 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")
if (!fs.existsSync(envFilePath)) {
const envFileJson = { const envFileJson = {
PORT: 4001, PORT: 4001,
MINIO_URL: "http://localhost:10000/", MINIO_URL: "http://localhost:10000/",
@ -48,14 +47,12 @@ async function init() {
COUCH_DB_PASSWORD: "budibase", COUCH_DB_PASSWORD: "budibase",
COUCH_DB_USER: "budibase", COUCH_DB_USER: "budibase",
SELF_HOSTED: 1, SELF_HOSTED: 1,
MULTI_TENANCY: "",
} }
let envFile = "" let envFile = ""
Object.keys(envFileJson).forEach(key => { Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n` envFile += `${key}=${envFileJson[key]}\n`
}) })
fs.writeFileSync(envFilePath, envFile) fs.writeFileSync(envFilePath, envFile)
}
} }
async function up() { async function up() {

View File

@ -10,7 +10,6 @@ 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)
@ -32,8 +31,8 @@ CREATE TABLE Products_Tasks (
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, Completed) VALUES (1, 'assembling', TRUE); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling');
INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'processing', FALSE); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing');
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

@ -1,8 +0,0 @@
#!/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,30 +1,8 @@
const { StaticDatabases } = require("@budibase/auth/db") const builderDB = require("../../db/builder")
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 getBuilderMainDoc() const mainDoc = await builderDB.getBuilderMainDoc()
ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {} ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {}
} catch (err) { } catch (err) {
/* istanbul ignore next */ /* istanbul ignore next */
@ -37,12 +15,12 @@ exports.update = async function (ctx) {
const value = ctx.request.body.value const value = ctx.request.body.value
try { try {
const mainDoc = await getBuilderMainDoc() const mainDoc = await builderDB.getBuilderMainDoc()
if (mainDoc.apiKeys == null) { if (mainDoc.apiKeys == null) {
mainDoc.apiKeys = {} mainDoc.apiKeys = {}
} }
mainDoc.apiKeys[key] = value mainDoc.apiKeys[key] = value
const resp = await setBuilderMainDoc(mainDoc) const resp = await builderDB.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("@budibase/auth/db") const { getAllApps } = require("../../utilities")
const { USERS_TABLE_SCHEMA } = require("../../constants") const { USERS_TABLE_SCHEMA } = require("../../constants")
const { const {
getDeployedApps, getDeployedApps,
@ -38,7 +38,6 @@ 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
@ -94,8 +93,7 @@ async function getAppUrlIfNotInUse(ctx) {
} }
async function createInstance(template) { async function createInstance(template) {
const tenantId = isMultiTenant() ? getTenantId() : null const baseAppId = generateAppID()
const baseAppId = generateAppID(tenantId)
const appId = generateDevAppID(baseAppId) const appId = generateDevAppID(baseAppId)
const db = new CouchDB(appId) const db = new CouchDB(appId)
@ -130,7 +128,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) {
@ -222,12 +220,10 @@ 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(),
} }
const response = await db.put(newApplication, { force: true }) await db.put(newApplication, { force: true })
newApplication._rev = response.rev
await createEmptyAppPackage(ctx, newApplication) await createEmptyAppPackage(ctx, newApplication)
/* istanbul ignore next */ /* istanbul ignore next */
@ -299,7 +295,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, ctx.params.appId) await removeAppFromUserRoles(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(ctx, userTable, { ctx.body = await outputProcessing(appId, userTable, {
...user, ...user,
...metadata, ...metadata,
}) })

View File

@ -1,6 +1,6 @@
const CouchDB = require("../../../db") const PouchDB = require("../../../db")
const Deployment = require("./Deployment") const Deployment = require("./Deployment")
const { Replication } = require("@budibase/auth/db") const { Replication, StaticDatabases } = 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,14 +31,13 @@ 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 CouchDB(appId) const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
let deploymentDoc let deploymentDoc
try { try {
// theres only one deployment doc per app database deploymentDoc = await db.get(appId)
deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
} catch (err) { } catch (err) {
deploymentDoc = { _id: DocumentTypes.DEPLOYMENTS, history: {} } deploymentDoc = { _id: appId, history: {} }
} }
const deploymentId = deploymentJSON._id const deploymentId = deploymentJSON._id
@ -68,7 +67,7 @@ async function deployApp(deployment) {
}) })
await replication.replicate() await replication.replicate()
const db = new CouchDB(productionAppId) const db = new PouchDB(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
@ -99,9 +98,8 @@ async function deployApp(deployment) {
exports.fetchDeployments = async function (ctx) { exports.fetchDeployments = async function (ctx) {
try { try {
const appId = ctx.appId const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
const db = new CouchDB(appId) const deploymentDoc = await db.get(ctx.appId)
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
const { updated, deployments } = await checkAllDeployments( const { updated, deployments } = await checkAllDeployments(
deploymentDoc, deploymentDoc,
ctx.user ctx.user
@ -117,9 +115,8 @@ exports.fetchDeployments = async function (ctx) {
exports.deploymentProgress = async function (ctx) { exports.deploymentProgress = async function (ctx) {
try { try {
const appId = ctx.appId const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
const db = new CouchDB(appId) const deploymentDoc = await db.get(ctx.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,9 +9,8 @@ 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/global/${devPath}?${queryString}`), checkSlashesInUrl(`${env.WORKER_URL}/api/admin/${devPath}`),
request( request(
ctx, ctx,
{ {

View File

@ -161,7 +161,7 @@ exports.fetchView = async ctx => {
schema: {}, schema: {},
} }
} }
rows = await outputProcessing(ctx, table, response.rows) rows = await outputProcessing(appId, 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(ctx, table, rows) return outputProcessing(appId, 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(ctx, table, row) row = await outputProcessing(appId, 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(ctx, table, response.rows) response.rows = await outputProcessing(appId, 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(
ctx, appId,
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, ctx.appId) const global = await getGlobalUsers(ctx.appId)
const metadata = ( const metadata = (
await database.allDocs( await database.allDocs(
getUserMetadataParams(null, { getUserMetadataParams(null, {

View File

@ -1,6 +1,5 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } = const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth
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")
@ -10,13 +9,6 @@ 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({
@ -44,8 +36,6 @@ 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/global/:devPath(.*)", controller.redirectGet) .get("/api/admin/:devPath(.*)", controller.redirectGet)
.post("/api/global/:devPath(.*)", controller.redirectPost) .post("/api/admin/:devPath(.*)", controller.redirectPost)
.delete("/api/global/:devPath(.*)", controller.redirectDelete) .delete("/api/admin/: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({ appId: config.getAppId() }, table, [row]) const enriched = await outputProcessing(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,7 +3,6 @@ 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
@ -17,8 +16,8 @@ exports.getAllTableRows = async config => {
return req.body return req.body
} }
exports.clearAllApps = async (tenantId = TENANT_ID) => { exports.clearAllApps = async () => {
const req = { query: { status: AppStatus.DEV }, user: { tenantId } } const req = { query: { status: AppStatus.DEV } }
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: "Log", title: "URL",
}, },
}, },
required: ["text"], required: ["text"],

View File

@ -3,10 +3,6 @@ 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
@ -20,7 +16,6 @@ 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
@ -45,19 +40,8 @@ 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)
@ -67,16 +51,13 @@ class Orchestrator {
) )
// appId is always passed // appId is always passed
try { try {
let tenantId = app.tenantId || DEFAULT_TENANT_ID const outputs = await stepFn({
const outputs = await doInTenant(tenantId, () => {
return stepFn({
inputs: step.inputs, inputs: step.inputs,
appId: this._appId, appId: this._appId,
apiKey: automation.apiKey, apiKey: automation.apiKey,
emitter: this._emitter, emitter: this._emitter,
context: this._context, context: this._context,
}) })
})
if (step.stepId === FILTER_STEP_ID && !outputs.success) { if (step.stepId === FILTER_STEP_ID && !outputs.success) {
break break
} }

View File

@ -0,0 +1,38 @@
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(ctx, appId, links) { async function getFullLinkedDocs(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(ctx, 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(ctx, appId, users) const globalUsers = await getGlobalUsers(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,13 +166,12 @@ 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 {object} ctx The request which is looking for rows. * @param {string} appId The app in which the tables/rows/links exist.
* @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 (ctx, table, rows) => { exports.attachFullLinkedDocs = async (appId, 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
@ -183,7 +182,7 @@ exports.attachFullLinkedDocs = async (ctx, 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(ctx, appId, links) let linked = await getFullLinkedDocs(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,7 +34,6 @@ const DocumentTypes = {
DATASOURCE: "datasource", DATASOURCE: "datasource",
DATASOURCE_PLUS: "datasource_plus", DATASOURCE_PLUS: "datasource_plus",
QUERY: "query", QUERY: "query",
DEPLOYMENTS: "deployments",
} }
const ViewNames = { const ViewNames = {
@ -50,7 +49,13 @@ const SearchIndexes = {
ROWS: "rows", ROWS: "rows",
} }
exports.StaticDatabases = StaticDatabases exports.StaticDatabases = {
BUILDER: {
name: "builder-db",
baseDoc: "builder-doc",
},
...StaticDatabases,
}
const BudibaseInternalDB = { const BudibaseInternalDB = {
_id: "bb_internal", _id: "bb_internal",
@ -225,12 +230,8 @@ 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 = (tenantId = null) => { exports.generateAppID = () => {
let id = `${DocumentTypes.APP}${SEPARATOR}` return `${DocumentTypes.APP}${SEPARATOR}${newid()}`
if (tenantId) {
id += `${tenantId}${SEPARATOR}`
}
return `${id}${newid()}`
} }
/** /**
@ -239,8 +240,8 @@ exports.generateAppID = (tenantId = null) => {
*/ */
exports.generateDevAppID = appId => { exports.generateDevAppID = appId => {
const prefix = `${DocumentTypes.APP}${SEPARATOR}` const prefix = `${DocumentTypes.APP}${SEPARATOR}`
const rest = appId.split(prefix)[1] const uuid = appId.split(prefix)[1]
return `${DocumentTypes.APP_DEV}${SEPARATOR}${rest}` return `${DocumentTypes.APP_DEV}${SEPARATOR}${uuid}`
} }
/** /**

View File

@ -35,7 +35,6 @@ 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,6 +68,5 @@ module.exports = async (ctx, next) => {
) { ) {
setCookie(ctx, { appId }, Cookies.CurrentApp) setCookie(ctx, { appId }, Cookies.CurrentApp)
} }
return next() return next()
} }

View File

@ -10,19 +10,16 @@ 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 auth = require("@budibase/auth") const { StaticDatabases } = require("@budibase/auth/db")
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"
@ -55,7 +52,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, tenantId: TENANT_ID } request.user = { appId: this.appId }
request.query = {} request.query = {}
request.request = { request.request = {
body: config, body: config,
@ -68,7 +65,7 @@ class TestConfiguration {
} }
async globalUser(id = GLOBAL_USER_ID, builder = true, roles) { async globalUser(id = GLOBAL_USER_ID, builder = true, roles) {
const db = getGlobalDB(TENANT_ID) const db = new CouchDB(StaticDatabases.GLOBAL.name)
let existing let existing
try { try {
existing = await db.get(id) existing = await db.get(id)
@ -79,9 +76,8 @@ class TestConfiguration {
_id: id, _id: id,
...existing, ...existing,
roles: roles || {}, roles: roles || {},
tenantId: TENANT_ID,
} }
await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID }) await createASession(id, "sessionid")
if (builder) { if (builder) {
user.builder = { global: true } user.builder = { global: true }
} }
@ -111,7 +107,6 @@ 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,
@ -338,15 +333,11 @@ class TestConfiguration {
if (!email || !password) { if (!email || !password) {
await this.createUser() await this.createUser()
} }
await createASession(userId, { await createASession(userId, "sessionid")
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,8 +4,6 @@ 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,12 +1,13 @@
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) {
@ -33,20 +34,18 @@ 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 (ctx, appId, userId) => { exports.getGlobalUser = async (appId, userId) => {
const db = getGlobalDB() const db = CouchDB(StaticDatabases.GLOBAL.name)
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 (ctx, appId = null, users = null) => { exports.getGlobalUsers = async (appId = null, users = null) => {
const db = getGlobalDB() const db = CouchDB(StaticDatabases.GLOBAL.name)
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,5 +1,6 @@
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"
@ -7,6 +8,7 @@ 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,10 +7,8 @@ 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 = new Client(utils.Databases.DEV_LOCKS) devAppClient = await new Client(utils.Databases.DEV_LOCKS).init()
debounceClient = new Client(utils.Databases.DEBOUNCE) debounceClient = await new Client(utils.Databases.DEBOUNCE).init()
await devAppClient.init()
await debounceClient.init()
} }
exports.shutdown = async () => { exports.shutdown = async () => {

View File

@ -193,21 +193,20 @@ 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 {object} ctx the request which is looking for enriched rows. * @param {string} appId the ID of the application for which rows are being enriched.
* @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 (ctx, table, rows) => { exports.outputProcessing = async (appId, 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(ctx, table, rows) let enriched = await linkRows.attachFullLinkedDocs(appId, 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, ctx.appId, userId) const global = await getGlobalUser(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,17 +4,13 @@ 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) { function request(ctx, request, noApiKey) {
if (!request.headers) { if (!request.headers) {
request.headers = {} request.headers = {}
} }
if (!ctx) { if (!noApiKey) {
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"
@ -33,11 +29,9 @@ function request(ctx, request) {
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/global/email/send`), checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`),
request(null, { request(null, {
method: "POST", method: "POST",
body: { body: {
@ -80,11 +74,11 @@ exports.getDeployedApps = async ctx => {
} }
exports.getGlobalSelf = async (ctx, appId = null) => { exports.getGlobalSelf = async (ctx, appId = null) => {
const endpoint = `/api/global/users/self` const endpoint = `/api/admin/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" }) request(ctx, { method: "GET" }, true)
) )
if (response.status !== 200) { if (response.status !== 200) {
ctx.throw(400, "Unable to get self globally.") ctx.throw(400, "Unable to get self globally.")
@ -102,11 +96,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/global/users/self` endpoint = `/api/admin/users/self`
} else { } else {
user = await getGlobalUser(ctx, appId, userId) user = await getGlobalUser(appId, userId)
body._id = userId body._id = userId
endpoint = `/api/global/users` endpoint = `/api/admin/users`
} }
body = { body = {
...body, ...body,
@ -128,11 +122,11 @@ exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => {
return response.json() return response.json()
} }
exports.removeAppFromUserRoles = async (ctx, appId) => { exports.removeAppFromUserRoles = async appId => {
const deployedAppId = getDeployedAppID(appId) const deployedAppId = getDeployedAppID(appId)
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/global/roles/${deployedAppId}`), checkSlashesInUrl(env.WORKER_URL + `/api/admin/roles/${deployedAppId}`),
request(ctx, { request(null, {
method: "DELETE", method: "DELETE",
}) })
) )

View File

@ -1146,11 +1146,12 @@
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.79-alpha.4": "@budibase/auth@^0.9.80-alpha.7":
version "0.9.79" version "0.9.80-alpha.7"
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.79.tgz#416271ffc55e84116550469656bf151a7734a90f" resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.80-alpha.7.tgz#6fb4c40a5f437bb9f7e49c9acafbc601b0dffa49"
integrity sha512-ENh099tYeUfVExsAeoxwMh2ODioKQGPteK9LJiU5hMdM4Oi7pyImu287BgKpTIheB+WtadT4e21VpPaJ62APEw== integrity sha512-9KZy8hqdpaWRY2n3pRAThP4Jb9TsrfJsJFdfDndJtPO1tTNKtDw2LGEwrT5Kym0a0SBHEzVrXq1Vw/sg72ACIQ==
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"
@ -1167,10 +1168,10 @@
uuid "^8.3.2" uuid "^8.3.2"
zlib "^1.0.5" zlib "^1.0.5"
"@budibase/bbui@^0.9.79": "@budibase/bbui@^0.9.80-alpha.7":
version "0.9.79" version "0.9.80-alpha.7"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.79.tgz#c033ba0af41cb584d2657a8353f9887328f6633f" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.80-alpha.7.tgz#5fbb7a6617a35a560151377fdc67a845f5620803"
integrity sha512-XxUJSPGd2FZDFdbNOeMUXohhID5h3DVq9XyKTe6WhYax4m2da/2WTENJ16UFvmfA+yxLN1qSDeweq9vw2zCahQ== integrity sha512-VJPP6A3BhxsLQzEfKPz3alCiT0nMqeM75P/reT1jsRxZsOCJ8vFn7g2c8aH2bEIcCqOWeUaaxVDuj8ghbzByUw==
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"
@ -1215,14 +1216,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.79-alpha.4": "@budibase/client@^0.9.80-alpha.7":
version "0.9.79" version "0.9.80-alpha.7"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.79.tgz#d1c8d51e9121f81902cfb31d3b685c8061f272a2" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.80-alpha.7.tgz#9d2e98b90cd9fdcfc659826d19b5dc206cdcfe7d"
integrity sha512-//Yqm5Qki6BmBe5W2Tz8GONdkFjdD1jkIU7pcLYKqdZJWEQIrX6T/xNvYvZVhw7Dx5bwSZRjFwzm7jLoiyHBIA== integrity sha512-szLz2JpWI9ZMyVz7IPap1fQ7e+uphuthOkOsERplmq4EXbv914/YILdEfUm01s4aeOEOdkeogz31t8t75es6Dg==
dependencies: dependencies:
"@budibase/bbui" "^0.9.79" "@budibase/bbui" "^0.9.80-alpha.7"
"@budibase/standard-components" "^0.9.79" "@budibase/standard-components" "^0.9.80-alpha.7"
"@budibase/string-templates" "^0.9.79" "@budibase/string-templates" "^0.9.80-alpha.7"
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"
@ -1255,24 +1256,26 @@
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.79", "@budibase/standard-components@^0.9.79-alpha.4": "@budibase/standard-components@^0.9.80-alpha.7":
version "0.9.79" version "0.9.80-alpha.7"
resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.79.tgz#24206642e0cdc655ea3a99ed5e9402ec4f6b3ba8" resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.80-alpha.7.tgz#17f13a25bfcda873f44d1460493325adcfe6f188"
integrity sha512-ZWhmBZ1iG+CjGMEvT/jtugMMgA1n88UYcOfP3BSP2P3eA16DubyU9hH9OyJHbGPzDHLoBF6vuS/5ZPZCkOKppw== integrity sha512-ohEVqhRxp2FeOlEnJtfBhyqtwmRGI/qPGs0K9FQfLQglMYJtPN5FgMrJ1gtN0W3zn7TOfNFnTcQIxIdLxSLwyA==
dependencies: dependencies:
"@budibase/bbui" "^0.9.79" "@budibase/bbui" "^0.9.80-alpha.7"
"@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.79", "@budibase/string-templates@^0.9.79-alpha.4": "@budibase/string-templates@^0.9.80-alpha.7":
version "0.9.79" version "0.9.80-alpha.7"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.79.tgz#bb75a7433a7cfda1fc488283f35e47879b799fcc" resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.80-alpha.7.tgz#10b06fc8652c00065f8928caebfcd0d143660078"
integrity sha512-hkAne5mx7mj8+osXFt45VwgLKSa94uQOGOb4R8uv9WNzvk4RzcjBfRzJxggv29FUemItrAeZpSh+Um6yugFI+w== integrity sha512-lD3BSWXW6PrdAbZcpVXSsr/fA8NdwvQ8W7T4chQ661UUMKVOYLnGwAvvAOArGpkdzSOAfSEuzgIB0+pBc92qWQ==
dependencies: dependencies:
"@budibase/handlebars-helpers" "^0.11.4" "@budibase/handlebars-helpers" "^0.11.4"
dayjs "^1.10.4" dayjs "^1.10.4"
@ -2111,6 +2114,11 @@
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"
@ -2270,7 +2278,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.1", "@spectrum-css/typography@^3.0.2":
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==
@ -2292,6 +2300,17 @@
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"
@ -3286,7 +3305,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.x.x, base64url@^3.0.1:
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==
@ -8707,7 +8726,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.x, oauth@^0.9.15:
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=
@ -10081,7 +10100,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.52.0", request@^2.72.0, request@^2.74.0, request@^2.87.0, request@^2.88.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==
@ -10205,7 +10224,7 @@ rimraf@2.6.3:
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
rimraf@^3.0.0: rimraf@^3.0.0, rimraf@^3.0.2:
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==
@ -10290,7 +10309,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.6.0, sax@^1.2.4: sax@>=0.1.1, 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==
@ -10745,6 +10764,11 @@ 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"
@ -11600,11 +11624,6 @@ 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"
@ -11786,6 +11805,14 @@ 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"
@ -11992,6 +12019,13 @@ 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,9 +16,7 @@
"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",
@ -54,8 +52,7 @@
"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,7 +4,6 @@ const fs = require("fs")
async function init() { async function init() {
const envFilePath = path.join(process.cwd(), ".env") const envFilePath = path.join(process.cwd(), ".env")
if (!fs.existsSync(envFilePath)) {
const envFileJson = { const envFileJson = {
SELF_HOSTED: 1, SELF_HOSTED: 1,
PORT: 4002, PORT: 4002,
@ -13,19 +12,18 @@ async function init() {
INTERNAL_API_KEY: "budibase", INTERNAL_API_KEY: "budibase",
MINIO_ACCESS_KEY: "budibase", MINIO_ACCESS_KEY: "budibase",
MINIO_SECRET_KEY: "budibase", MINIO_SECRET_KEY: "budibase",
COUCH_DB_USER: "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 = "" let envFile = ""
Object.keys(envFileJson).forEach(key => { Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n` envFile += `${key}=${envFileJson[key]}\n`
}) })
fs.writeFileSync(envFilePath, envFile) 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,4 +3,3 @@ 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

@ -1,8 +0,0 @@
#!/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

@ -2,27 +2,15 @@ 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")
function googleCallbackUrl() { const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
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) {
@ -78,7 +66,6 @@ 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 = {
@ -93,7 +80,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 = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
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)
@ -115,14 +102,12 @@ 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 = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
let callbackUrl = googleCallbackUrl()
const config = await authPkg.db.getScopedConfig(db, { const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE, type: Configs.GOOGLE,
workspace: ctx.query.workspace, group: ctx.query.group,
}) })
const strategy = await google.strategyFactory(config, callbackUrl) const strategy = await google.strategyFactory(config)
return passport.authenticate(strategy, { return passport.authenticate(strategy, {
scope: ["profile", "email"], scope: ["profile", "email"],
@ -130,14 +115,13 @@ exports.googlePreAuth = async (ctx, next) => {
} }
exports.googleAuth = async (ctx, next) => { exports.googleAuth = async (ctx, next) => {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
const callbackUrl = googleCallbackUrl()
const config = await authPkg.db.getScopedConfig(db, { const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE, type: Configs.GOOGLE,
workspace: ctx.query.workspace, group: ctx.query.group,
}) })
const strategy = await google.strategyFactory(config, callbackUrl) const strategy = await google.strategyFactory(config)
return passport.authenticate( return passport.authenticate(
strategy, strategy,
@ -151,7 +135,8 @@ exports.googleAuth = async (ctx, next) => {
} }
async function oidcStrategyFactory(ctx, configId) { async function oidcStrategyFactory(ctx, configId) {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
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,
@ -159,12 +144,9 @@ 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]
const protocol = env.NODE_ENV === "production" ? "https" : "http" // require https callback in production
let callbackUrl = `${protocol}://${ctx.host}/api/global/auth` const protocol = process.env.NODE_ENV === "production" ? "https" : "http"
if (isMultiTenant()) { const callbackUrl = `${protocol}://${ctx.host}/api/admin/auth/oidc/callback`
callbackUrl += `/${getTenantId()}`
}
callbackUrl += `/oidc/callback`
return oidc.strategyFactory(chosenConfig, callbackUrl) return oidc.strategyFactory(chosenConfig, callbackUrl)
} }

View File

@ -1,25 +1,28 @@
const CouchDB = require("../../../db")
const { const {
generateConfigID, generateConfigID,
StaticDatabases,
getConfigParams, getConfigParams,
getGlobalUserParams, getGlobalUserParams,
getScopedFullConfig, getScopedFullConfig,
getAllApps, } = require("@budibase/auth").db
} = 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 { getGlobalDB } = require("@budibase/auth/tenancy") const APP_PREFIX = "app_"
const GLOBAL_DB = StaticDatabases.GLOBAL.name
exports.save = async function (ctx) { exports.save = async function (ctx) {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
const { type, workspace, user, config } = ctx.request.body const { type, group, 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,
workspace, group,
user, user,
}) })
} }
@ -48,7 +51,7 @@ exports.save = async function (ctx) {
} }
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
const response = await db.allDocs( const response = await db.allDocs(
getConfigParams( getConfigParams(
{ type: ctx.params.type }, { type: ctx.params.type },
@ -62,19 +65,17 @@ 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 -> workspace -> user. * The hierarchy is type -> group -> user.
*/ */
exports.find = async function (ctx) { exports.find = async function (ctx) {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
const { userId, workspaceId } = ctx.query const { userId, groupId } = ctx.query
if (workspaceId && userId) { if (groupId && userId) {
const workspace = await db.get(workspaceId) const group = await db.get(groupId)
const userInWorkspace = workspace.users.some( const userInGroup = group.users.some(groupUser => groupUser === userId)
workspaceUser => workspaceUser === userId if (!ctx.user.admin && !userInGroup) {
) 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}.`)
} }
} }
@ -83,7 +84,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,
workspace: workspaceId, group: groupId,
}) })
if (scopedConfig) { if (scopedConfig) {
@ -98,7 +99,7 @@ exports.find = async function (ctx) {
} }
exports.publicOidc = async function (ctx) { exports.publicOidc = async function (ctx) {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
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, {
@ -108,11 +109,14 @@ exports.publicOidc = async function (ctx) {
if (!oidcConfig) { if (!oidcConfig) {
ctx.body = {} ctx.body = {}
} else { } else {
ctx.body = oidcConfig.config.configs.map(config => ({ const partialOidcCofig = 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)
@ -120,7 +124,7 @@ exports.publicOidc = async function (ctx) {
} }
exports.publicSettings = async function (ctx) { exports.publicSettings = async function (ctx) {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
try { try {
// Find the config with the most granular scope based on context // Find the config with the most granular scope based on context
@ -136,7 +140,7 @@ exports.publicSettings = async function (ctx) {
type: Configs.OIDC, type: Configs.OIDC,
}) })
let config let config = {}
if (!publicConfig) { if (!publicConfig) {
config = { config = {
config: {}, config: {},
@ -147,16 +151,18 @@ exports.publicSettings = async function (ctx) {
// google button flag // google button flag
if (googleConfig && googleConfig.config) { if (googleConfig && googleConfig.config) {
// activated by default for configs pre-activated flag const googleActivated =
config.config.google = googleConfig.config.activated == undefined || // activated by default for configs pre-activated flag
googleConfig.config.activated == null || googleConfig.config.activated 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) {
config.config.oidc = oidcConfig.config.configs[0].activated const oidcActivated = oidcConfig.config.configs[0].activated
config.config.oidc = oidcActivated
} else { } else {
config.config.oidc = false config.config.oidc = false
} }
@ -185,7 +191,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 = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
let cfgStructure = await getScopedFullConfig(db, { type }) let cfgStructure = await getScopedFullConfig(db, { type })
if (!cfgStructure) { if (!cfgStructure) {
cfgStructure = { cfgStructure = {
@ -205,7 +211,7 @@ exports.upload = async function (ctx) {
} }
exports.destroy = async function (ctx) { exports.destroy = async function (ctx) {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
const { id, rev } = ctx.params const { id, rev } = ctx.params
try { try {
@ -217,13 +223,14 @@ exports.destroy = async function (ctx) {
} }
exports.configChecklist = async function (ctx) { exports.configChecklist = async function (ctx) {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
try { try {
// TODO: Watch get started video // TODO: Watch get started video
// Apps exist // Apps exist
const apps = await getAllApps(CouchDB) let allDbs = await CouchDB.allDbs()
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, {
@ -239,7 +246,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 global user // They have set up an admin user
const users = await db.allDocs( const users = await db.allDocs(
getGlobalUserParams(null, { getGlobalUserParams(null, {
include_docs: true, include_docs: true,
@ -248,7 +255,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: apps.length, apps: appDbNames.length,
smtp: !!smtpConfig, smtp: !!smtpConfig,
adminUser, adminUser,
sso: !!googleConfig || !!oidcConfig, sso: !!googleConfig || !!oidcConfig,

View File

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

View File

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

View File

@ -7,9 +7,8 @@ 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, { tenantId, all: true }) const apps = await getAllApps({ CouchDB, 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,14 +1,16 @@
const { generateTemplateID } = require("@budibase/auth/db") const { generateTemplateID, StaticDatabases } = 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 = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
let template = ctx.request.body let template = ctx.request.body
if (!template.ownerId) { if (!template.ownerId) {
template.ownerId = GLOBAL_OWNER template.ownerId = GLOBAL_OWNER
@ -68,7 +70,7 @@ exports.find = async ctx => {
} }
exports.destroy = async ctx => { exports.destroy = async ctx => {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
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,30 +1,17 @@
const { const CouchDB = require("../../../db")
generateGlobalUserID, const { generateGlobalUserID, getGlobalUserParams, StaticDatabases } =
getGlobalUserParams, require("@budibase/auth").db
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 PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name const GLOBAL_DB = StaticDatabases.GLOBAL.name
async function allUsers() { async function allUsers() {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
const response = await db.allDocs( const response = await db.allDocs(
getGlobalUserParams(null, { getGlobalUserParams(null, {
include_docs: true, include_docs: true,
@ -33,21 +20,16 @@ async function allUsers() {
return response.rows.map(row => row.doc) return response.rows.map(row => row.doc)
} }
async function saveUser(user, tenantId) { exports.save = async ctx => {
if (!tenantId) { const db = new CouchDB(GLOBAL_DB)
throw "No tenancy specified." const { email, password, _id } = ctx.request.body
}
// 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))) {
throw "Email address already in use." ctx.throw(400, "Email address already in use.")
} }
} else { } else {
dbUser = await db.get(_id) dbUser = await db.get(_id)
@ -60,16 +42,14 @@ async function saveUser(user, tenantId) {
} else if (dbUser) { } else if (dbUser) {
hashedPassword = dbUser.password hashedPassword = dbUser.password
} else { } else {
throw "Password must be specified." ctx.throw(400, "Password must be specified.")
} }
_id = _id || generateGlobalUserID() let user = {
user = {
...dbUser, ...dbUser,
...user, ...ctx.request.body,
_id, _id: _id || generateGlobalUserID(),
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) {
@ -84,37 +64,23 @@ async function saveUser(user, tenantId) {
password: hashedPassword, password: hashedPassword,
...user, ...user,
}) })
await tryAddTenant(tenantId, _id, email)
await userCache.invalidateUser(response.id) await userCache.invalidateUser(response.id)
return { ctx.body = {
_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) {
throw "User exists already" ctx.throw(400, "User exists already")
} else { } else {
throw err ctx.throw(err.status, 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 { email, password, tenantId } = ctx.request.body const db = new CouchDB(GLOBAL_DB)
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,
@ -122,13 +88,11 @@ exports.adminUser = async ctx => {
) )
if (response.rows.some(row => row.doc.admin)) { if (response.rows.some(row => row.doc.admin)) {
ctx.throw( ctx.throw(403, "You cannot initialise once an admin user has been created.")
403,
"You cannot initialise once an global user has been created."
)
} }
const user = { const { email, password } = ctx.request.body
ctx.request.body = {
email: email, email: email,
password: password, password: password,
roles: {}, roles: {},
@ -138,17 +102,12 @@ 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 = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
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)
@ -160,8 +119,8 @@ exports.destroy = async ctx => {
exports.removeAppRole = async ctx => { exports.removeAppRole = async ctx => {
const { appId } = ctx.params const { appId } = ctx.params
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
const users = await allUsers(ctx) const users = await allUsers()
const bulk = [] const bulk = []
const cacheInvalidations = [] const cacheInvalidations = []
for (let user of users) { for (let user of users) {
@ -190,7 +149,7 @@ exports.getSelf = async ctx => {
} }
exports.updateSelf = async ctx => { exports.updateSelf = async ctx => {
const db = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
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)
@ -211,7 +170,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(ctx) const users = await allUsers()
// 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) {
@ -223,7 +182,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 = getGlobalDB() const db = new CouchDB(GLOBAL_DB)
let user let user
try { try {
user = await db.get(ctx.params.id) user = await db.get(ctx.params.id)
@ -237,38 +196,12 @@ 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 => {
let { email, userInfo } = ctx.request.body const { 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,
@ -281,18 +214,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 global // info is an extension of the user object that was stored by admin
const { email, info } = await checkInviteCode(inviteCode) const { email, info } = await checkInviteCode(inviteCode)
ctx.body = await saveUser( // only pass through certain props for accepting
{ ctx.request.body = {
firstName, firstName,
lastName, lastName,
password, password,
email, email,
...info, ...info,
}, }
info.tenantId // this will flesh out the body response
) 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,12 +1,18 @@
const { getAllApps } = require("@budibase/auth/db") const { DocumentTypes } = 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 => {
const tenantId = ctx.user.tenantId // allDbs call of CouchDB is very inaccurate in production
const apps = await getAllApps(CouchDB, { tenantId }) const allDbs = await CouchDB.allDbs()
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

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

View File

@ -1,33 +0,0 @@
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,51 +2,55 @@ 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, buildTenancyMiddleware } = const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth
require("@budibase/auth").auth
const PUBLIC_ENDPOINTS = [ const PUBLIC_ENDPOINTS = [
{ {
// this covers all of the POST auth routes route: "/api/admin/users/init",
route: "/api/global/auth/:tenantId",
method: "POST", method: "POST",
}, },
{ {
// this covers all of the GET auth routes route: "/api/admin/users/invite/accept",
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/global/users/invite/accept", route: "/api/admin/auth",
method: "POST", method: "POST",
}, },
{ {
route: "api/system/flags", route: "/api/admin/auth/google",
method: "GET", method: "GET",
}, },
]
const NO_TENANCY_ENDPOINTS = [
...PUBLIC_ENDPOINTS,
{ {
route: "/api/system", route: "/api/admin/auth/google/callback",
method: "ALL", method: "GET",
}, },
{ {
route: "/api/global/users/self", route: "/api/admin/auth/oidc",
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",
}, },
] ]
@ -67,10 +71,9 @@ 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 && !ctx.publicEndpoint) { if (!ctx.isAuthenticated) {
ctx.throw(403, "Unauthorized - no public worker access") ctx.throw(403, "Unauthorized - no public worker access")
} }
return next() return next()

View File

@ -0,0 +1,45 @@
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,5 +1,5 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../../controllers/global/configs") const controller = require("../../controllers/admin/configs")
const joiValidator = require("../../../middleware/joi-validator") const joiValidator = require("../../../middleware/joi-validator")
const adminOnly = require("../../../middleware/adminOnly") const adminOnly = require("../../../middleware/adminOnly")
const Joi = require("joi") const Joi = require("joi")
@ -37,6 +37,7 @@ function googleValidation() {
return Joi.object({ return Joi.object({
clientID: Joi.string().required(), clientID: Joi.string().required(),
clientSecret: Joi.string().required(), clientSecret: Joi.string().required(),
callbackURL: Joi.string().required(),
activated: Joi.boolean().required(), activated: Joi.boolean().required(),
}).unknown(true) }).unknown(true)
} }
@ -63,7 +64,7 @@ function buildConfigSaveValidation() {
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
_id: Joi.string().optional(), _id: Joi.string().optional(),
_rev: Joi.string().optional(), _rev: Joi.string().optional(),
workspace: Joi.string().optional(), group: Joi.string().optional(),
type: Joi.string().valid(...Object.values(Configs)).required(), type: Joi.string().valid(...Object.values(Configs)).required(),
config: Joi.alternatives() config: Joi.alternatives()
.conditional("type", { .conditional("type", {
@ -96,24 +97,24 @@ function buildConfigGetValidation() {
router router
.post( .post(
"/api/global/configs", "/api/admin/configs",
adminOnly, adminOnly,
buildConfigSaveValidation(), buildConfigSaveValidation(),
controller.save controller.save
) )
.delete("/api/global/configs/:id/:rev", adminOnly, controller.destroy) .delete("/api/admin/configs/:id/:rev", adminOnly, controller.destroy)
.get("/api/global/configs", controller.fetch) .get("/api/admin/configs", controller.fetch)
.get("/api/global/configs/checklist", controller.configChecklist) .get("/api/admin/configs/checklist", controller.configChecklist)
.get( .get(
"/api/global/configs/all/:type", "/api/admin/configs/all/:type",
buildConfigGetValidation(), buildConfigGetValidation(),
controller.fetch controller.fetch
) )
.get("/api/global/configs/public", controller.publicSettings) .get("/api/admin/configs/public", controller.publicSettings)
.get("/api/global/configs/public/oidc", controller.publicOidc) .get("/api/admin/configs/publicOidc", controller.publicOidc)
.get("/api/global/configs/:type", buildConfigGetValidation(), controller.find) .get("/api/admin/configs/:type", buildConfigGetValidation(), controller.find)
.post( .post(
"/api/global/configs/upload/:type/:name", "/api/admin/configs/upload/:type/:name",
adminOnly, adminOnly,
buildUploadValidation(), buildUploadValidation(),
controller.upload controller.upload

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