diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js index bd4a762e1d..03d1d02e5b 100644 --- a/packages/auth/src/redis/utils.js +++ b/packages/auth/src/redis/utils.js @@ -9,6 +9,7 @@ const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD exports.Databases = { PW_RESETS: "pwReset", INVITATIONS: "invitation", + DEV_LOCKS: "devLocks", } exports.getRedisOptions = (clustered = false) => { diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 332b917a76..4557e82535 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -37,6 +37,7 @@ router }) ) .use(currentApp) + .use(development) // error handling middleware router.use(async (ctx, next) => { diff --git a/packages/server/src/app.js b/packages/server/src/app.js index e9f7a74ca6..9ec6c2c687 100644 --- a/packages/server/src/app.js +++ b/packages/server/src/app.js @@ -13,6 +13,7 @@ const automations = require("./automations/index") const Sentry = require("@sentry/node") const fileSystem = require("./utilities/fileSystem") const bullboard = require("./automations/bullboard") +const redis = require("./utilities/redis") const app = new Koa() @@ -84,6 +85,7 @@ module.exports = server.listen(env.PORT || 0, async () => { eventEmitter.emitPort(env.PORT) fileSystem.init() await automations.init() + await redis.init() }) process.on("uncaughtException", err => { diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index 5f027398a8..4d5b356da5 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -17,6 +17,7 @@ const DocumentTypes = { AUTOMATION: "au", LINK: "li", APP: "app", + APP_DEV: "app_dev", ROLE: "role", WEBHOOK: "wh", INSTANCE: "inst", @@ -39,6 +40,8 @@ const SearchIndexes = { ROWS: "rows", } +exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR +exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR exports.StaticDatabases = StaticDatabases exports.ViewNames = ViewNames exports.InternalTables = InternalTables @@ -138,9 +141,11 @@ exports.generateUserMetadataID = globalId => { * Breaks up the ID to get the global ID. */ exports.getGlobalIDFromUserMetadataID = id => { - return id.split( - `${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` - )[1] + const prefix = `${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` + if (!id.includes(prefix)) { + return id + } + return id.split(prefix)[1] } /** @@ -199,10 +204,13 @@ exports.generateAppID = () => { } /** - * Gets parameters for retrieving apps, this is a utility function for the getDocParams function. + * Generates a development app ID from a real app ID. + * @returns {string} the dev app ID which can be used for dev database. */ -exports.getAppParams = (appId = null, otherProps = {}) => { - return getDocParams(DocumentTypes.APP, appId, otherProps) +exports.generateDevAppID = appId => { + const prefix = `${DocumentTypes.APP}${SEPARATOR}` + const uuid = appId.split(prefix)[1] + return `${DocumentTypes.APP_DEV}${SEPARATOR}${uuid}` } /** diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index 589529fadc..29a1bd026b 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -4,6 +4,8 @@ const { doesHaveResourcePermission, doesHaveBasePermission, } = require("../utilities/security/permissions") +const { APP_DEV_PREFIX, getGlobalIDFromUserMetadataID } = require("../db/utils") +const { doesUserHaveLock, updateLock } = require("../utilities/redis") function hasResource(ctx) { return ctx.resourceId != null @@ -13,6 +15,23 @@ const WEBHOOK_ENDPOINTS = new RegExp( ["webhooks/trigger", "webhooks/schema"].join("|") ) +async function checkDevAppLocks(ctx) { + const appId = ctx.appId + + // not a development app, don't need to do anything + if (!appId.startsWith(APP_DEV_PREFIX)) { + return + } + // get the user which is currently using the dev app + const userId = getGlobalIDFromUserMetadataID(ctx.user._id) + if (!await doesUserHaveLock(appId, userId)) { + ctx.throw(403, "User does not hold app lock.") + } + + // they do have lock, update it + await updateLock(appId, userId) +} + module.exports = (permType, permLevel = null) => async (ctx, next) => { // webhooks don't need authentication, each webhook unique if (WEBHOOK_ENDPOINTS.test(ctx.request.url)) { @@ -23,8 +42,14 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => { return ctx.throw(403, "No user info found") } - const isAuthed = ctx.isAuthenticated + const builderCall = permType === PermissionTypes.BUILDER + // this makes sure that builder calls abide by dev locks + if (builderCall) { + await checkDevAppLocks(ctx) + } + + const isAuthed = ctx.isAuthenticated const { basePermissions, permissions } = await getUserPermissions( ctx.appId, ctx.roleId @@ -35,7 +60,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => { let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global if (isBuilder) { return next() - } else if (permType === PermissionTypes.BUILDER && !isBuilder) { + } else if (builderCall && !isBuilder) { return ctx.throw(403, "Not Authorized") } diff --git a/packages/server/src/utilities/index.js b/packages/server/src/utilities/index.js index 4210e6678b..ac3b668ad5 100644 --- a/packages/server/src/utilities/index.js +++ b/packages/server/src/utilities/index.js @@ -1,10 +1,9 @@ const env = require("../environment") -const { DocumentTypes, SEPARATOR } = require("../db/utils") +const { APP_PREFIX } = require("../db/utils") const CouchDB = require("../db") const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants") const BB_CDN = "https://cdn.app.budi.live/assets" -const APP_PREFIX = DocumentTypes.APP + SEPARATOR exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms)) diff --git a/packages/server/src/utilities/redis.js b/packages/server/src/utilities/redis.js new file mode 100644 index 0000000000..4df61f33fb --- /dev/null +++ b/packages/server/src/utilities/redis.js @@ -0,0 +1,31 @@ +const { Client, utils } = require("@budibase/auth").redis + +const APP_DEV_LOCK_SECONDS = 600 +const DB_NAME = utils.Databases.DEV_LOCKS +let devAppClient + +// we init this as we want to keep the connection open all the time +// reduces the performance hit +exports.init = async () => { + devAppClient = await (new Client(DB_NAME)).init() +} + +exports.doesUserHaveLock = async (devAppId, userId) => { + const value = await devAppClient.get(devAppId) + return value == null || value === userId +} + +exports.updateLock = async (devAppId, userId) => { + await devAppClient.store(devAppId, userId, APP_DEV_LOCK_SECONDS) +} + +exports.clearLock = async (devAppId, userId) => { + const value = await devAppClient.get(devAppId) + if (!value) { + return + } + if (value !== userId) { + throw "User does not hold lock, cannot clear it." + } + await devAppClient.delete(devAppClient) +}