Merge pull request #5134 from Budibase/fix/5103

Fix for RBAC on apps built from templates containing public screens
This commit is contained in:
Michael Drury 2022-03-28 17:17:19 +01:00 committed by GitHub
commit 9890b45d71
4 changed files with 62 additions and 16 deletions

View File

@ -1,5 +1,5 @@
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
const { BUILTIN_PERMISSION_IDS } = require("./permissions") const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions")
const { const {
generateRoleID, generateRoleID,
getRoleParams, getRoleParams,
@ -180,6 +180,20 @@ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
return opts.idOnly ? roles.map(role => role._id) : roles return opts.idOnly ? roles.map(role => role._id) : roles
} }
// this function checks that the provided permissions are in an array format
// some templates/older apps will use a simple string instead of array for roles
// convert the string to an array using the theory that write is higher than read
exports.checkForRoleResourceArray = (rolePerms, resourceId) => {
if (rolePerms && !Array.isArray(rolePerms[resourceId])) {
const permLevel = rolePerms[resourceId]
rolePerms[resourceId] = [permLevel]
if (permLevel === PermissionLevels.WRITE) {
rolePerms[resourceId].push(PermissionLevels.READ)
}
}
return rolePerms
}
/** /**
* Given an app ID this will retrieve all of the roles that are currently within that app. * Given an app ID this will retrieve all of the roles that are currently within that app.
* @return {Promise<object[]>} An array of the role objects that were found. * @return {Promise<object[]>} An array of the role objects that were found.
@ -209,15 +223,27 @@ exports.getAllRoles = async appId => {
roles.push(Object.assign(builtinRole, dbBuiltin)) roles.push(Object.assign(builtinRole, dbBuiltin))
} }
} }
// check permissions
for (let role of roles) {
if (!role.permissions) {
continue
}
for (let resourceId of Object.keys(role.permissions)) {
role.permissions = exports.checkForRoleResourceArray(
role.permissions,
resourceId
)
}
}
return roles return roles
} }
/** /**
* This retrieves the required role * This retrieves the required role for a resource
* @param permLevel * @param permLevel The level of request
* @param resourceId * @param resourceId The resource being requested
* @param subResourceId * @param subResourceId The sub resource being requested
* @return {Promise<{permissions}|Object>} * @return {Promise<{permissions}|Object>} returns the permissions required to access.
*/ */
exports.getRequiredResourceRole = async ( exports.getRequiredResourceRole = async (
permLevel, permLevel,

View File

@ -4,6 +4,7 @@ const {
getDBRoleID, getDBRoleID,
getExternalRoleID, getExternalRoleID,
getBuiltinRoles, getBuiltinRoles,
checkForRoleResourceArray,
} = require("@budibase/backend-core/roles") } = require("@budibase/backend-core/roles")
const { getRoleParams } = require("../../db/utils") const { getRoleParams } = require("../../db/utils")
const { const {
@ -144,12 +145,11 @@ exports.getResourcePerms = async function (ctx) {
for (let level of SUPPORTED_LEVELS) { for (let level of SUPPORTED_LEVELS) {
// update the various roleIds in the resource permissions // update the various roleIds in the resource permissions
for (let role of roles) { for (let role of roles) {
const rolePerms = role.permissions const rolePerms = checkForRoleResourceArray(role.permissions, resourceId)
if ( if (
rolePerms && rolePerms &&
rolePerms[resourceId] && rolePerms[resourceId] &&
(rolePerms[resourceId] === level || rolePerms[resourceId].indexOf(level) !== -1
rolePerms[resourceId].indexOf(level) !== -1)
) { ) {
permissions[level] = getExternalRoleID(role._id) permissions[level] = getExternalRoleID(role._id)
} }

View File

@ -40,7 +40,7 @@ router
.get( .get(
"/api/tables/:tableId", "/api/tables/:tableId",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionTypes.TABLE, PermissionLevels.READ, { schema: true }),
tableController.find tableController.find
) )
/** /**

View File

@ -5,6 +5,7 @@ const {
} = require("@budibase/backend-core/roles") } = require("@budibase/backend-core/roles")
const { const {
PermissionTypes, PermissionTypes,
PermissionLevels,
doesHaveBasePermission, doesHaveBasePermission,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const builderMiddleware = require("./builder") const builderMiddleware = require("./builder")
@ -64,7 +65,7 @@ const checkAuthorizedResource = async (
} }
module.exports = module.exports =
(permType, permLevel = null) => (permType, permLevel = null, opts = { schema: false }) =>
async (ctx, next) => { async (ctx, next) => {
// webhooks don't need authentication, each webhook unique // webhooks don't need authentication, each webhook unique
// also internal requests (between services) don't need authorized // also internal requests (between services) don't need authorized
@ -81,15 +82,25 @@ module.exports =
await builderMiddleware(ctx, permType) await builderMiddleware(ctx, permType)
// get the resource roles // get the resource roles
let resourceRoles = [] let resourceRoles = [],
otherLevelRoles
const otherLevel =
permLevel === PermissionLevels.READ
? PermissionLevels.WRITE
: PermissionLevels.READ
const appId = getAppId() const appId = getAppId()
if (appId && hasResource(ctx)) { if (appId && hasResource(ctx)) {
resourceRoles = await getRequiredResourceRole(permLevel, ctx) resourceRoles = await getRequiredResourceRole(permLevel, ctx)
if (opts && opts.schema) {
otherLevelRoles = await getRequiredResourceRole(otherLevel, ctx)
}
} }
// if the resource is public, proceed // if the resource is public, proceed
const isPublicResource = resourceRoles.includes(BUILTIN_ROLE_IDS.PUBLIC) if (
if (isPublicResource) { resourceRoles.includes(BUILTIN_ROLE_IDS.PUBLIC) ||
(otherLevelRoles && otherLevelRoles.includes(BUILTIN_ROLE_IDS.PUBLIC))
) {
return next() return next()
} }
@ -98,8 +109,17 @@ module.exports =
return ctx.throw(403, "Session not authenticated") return ctx.throw(403, "Session not authenticated")
} }
try {
// check authorized // check authorized
await checkAuthorized(ctx, resourceRoles, permType, permLevel) await checkAuthorized(ctx, resourceRoles, permType, permLevel)
} catch (err) {
// this is a schema, check if
if (opts && opts.schema && permLevel) {
await checkAuthorized(ctx, otherLevelRoles, permType, otherLevel)
} else {
throw err
}
}
// csrf protection // csrf protection
return csrf(ctx, next) return csrf(ctx, next)