Putting together redis lock system.

This commit is contained in:
mike12345567 2021-05-12 17:37:09 +01:00
parent 8f7a3f4d69
commit f6fbeb4858
7 changed files with 77 additions and 10 deletions

View File

@ -9,6 +9,7 @@ const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD
exports.Databases = { exports.Databases = {
PW_RESETS: "pwReset", PW_RESETS: "pwReset",
INVITATIONS: "invitation", INVITATIONS: "invitation",
DEV_LOCKS: "devLocks",
} }
exports.getRedisOptions = (clustered = false) => { exports.getRedisOptions = (clustered = false) => {

View File

@ -37,6 +37,7 @@ router
}) })
) )
.use(currentApp) .use(currentApp)
.use(development)
// error handling middleware // error handling middleware
router.use(async (ctx, next) => { router.use(async (ctx, next) => {

View File

@ -13,6 +13,7 @@ const automations = require("./automations/index")
const Sentry = require("@sentry/node") const Sentry = require("@sentry/node")
const fileSystem = require("./utilities/fileSystem") const fileSystem = require("./utilities/fileSystem")
const bullboard = require("./automations/bullboard") const bullboard = require("./automations/bullboard")
const redis = require("./utilities/redis")
const app = new Koa() const app = new Koa()
@ -84,6 +85,7 @@ module.exports = server.listen(env.PORT || 0, async () => {
eventEmitter.emitPort(env.PORT) eventEmitter.emitPort(env.PORT)
fileSystem.init() fileSystem.init()
await automations.init() await automations.init()
await redis.init()
}) })
process.on("uncaughtException", err => { process.on("uncaughtException", err => {

View File

@ -17,6 +17,7 @@ const DocumentTypes = {
AUTOMATION: "au", AUTOMATION: "au",
LINK: "li", LINK: "li",
APP: "app", APP: "app",
APP_DEV: "app_dev",
ROLE: "role", ROLE: "role",
WEBHOOK: "wh", WEBHOOK: "wh",
INSTANCE: "inst", INSTANCE: "inst",
@ -39,6 +40,8 @@ const SearchIndexes = {
ROWS: "rows", ROWS: "rows",
} }
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
exports.StaticDatabases = StaticDatabases exports.StaticDatabases = StaticDatabases
exports.ViewNames = ViewNames exports.ViewNames = ViewNames
exports.InternalTables = InternalTables exports.InternalTables = InternalTables
@ -138,9 +141,11 @@ exports.generateUserMetadataID = globalId => {
* Breaks up the ID to get the global ID. * Breaks up the ID to get the global ID.
*/ */
exports.getGlobalIDFromUserMetadataID = id => { exports.getGlobalIDFromUserMetadataID = id => {
return id.split( const prefix = `${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}`
`${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` if (!id.includes(prefix)) {
)[1] 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 = {}) => { exports.generateDevAppID = appId => {
return getDocParams(DocumentTypes.APP, appId, otherProps) const prefix = `${DocumentTypes.APP}${SEPARATOR}`
const uuid = appId.split(prefix)[1]
return `${DocumentTypes.APP_DEV}${SEPARATOR}${uuid}`
} }
/** /**

View File

@ -4,6 +4,8 @@ const {
doesHaveResourcePermission, doesHaveResourcePermission,
doesHaveBasePermission, doesHaveBasePermission,
} = require("../utilities/security/permissions") } = require("../utilities/security/permissions")
const { APP_DEV_PREFIX, getGlobalIDFromUserMetadataID } = require("../db/utils")
const { doesUserHaveLock, updateLock } = require("../utilities/redis")
function hasResource(ctx) { function hasResource(ctx) {
return ctx.resourceId != null return ctx.resourceId != null
@ -13,6 +15,23 @@ const WEBHOOK_ENDPOINTS = new RegExp(
["webhooks/trigger", "webhooks/schema"].join("|") ["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) => { module.exports = (permType, permLevel = null) => async (ctx, next) => {
// webhooks don't need authentication, each webhook unique // webhooks don't need authentication, each webhook unique
if (WEBHOOK_ENDPOINTS.test(ctx.request.url)) { 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") 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( const { basePermissions, permissions } = await getUserPermissions(
ctx.appId, ctx.appId,
ctx.roleId 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 let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
if (isBuilder) { if (isBuilder) {
return next() return next()
} else if (permType === PermissionTypes.BUILDER && !isBuilder) { } else if (builderCall && !isBuilder) {
return ctx.throw(403, "Not Authorized") return ctx.throw(403, "Not Authorized")
} }

View File

@ -1,10 +1,9 @@
const env = require("../environment") const env = require("../environment")
const { DocumentTypes, SEPARATOR } = require("../db/utils") const { APP_PREFIX } = require("../db/utils")
const CouchDB = require("../db") const CouchDB = require("../db")
const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants") const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants")
const BB_CDN = "https://cdn.app.budi.live/assets" const BB_CDN = "https://cdn.app.budi.live/assets"
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms)) exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms))

View File

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