Putting together redis lock system.
This commit is contained in:
parent
abeb78d914
commit
2021c59dd9
|
@ -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) => {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue