Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for RBAC on apps built from templates containing public screens #5134

Merged
merged 1 commit into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions packages/backend-core/src/security/roles.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { cloneDeep } = require("lodash/fp")
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions")
const {
generateRoleID,
getRoleParams,
Expand Down Expand Up @@ -180,6 +180,20 @@ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
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.
* @return {Promise<object[]>} An array of the role objects that were found.
Expand Down Expand Up @@ -209,15 +223,27 @@ exports.getAllRoles = async appId => {
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
}

/**
* This retrieves the required role
* @param permLevel
* @param resourceId
* @param subResourceId
* @return {Promise<{permissions}|Object>}
* This retrieves the required role for a resource
* @param permLevel The level of request
* @param resourceId The resource being requested
* @param subResourceId The sub resource being requested
* @return {Promise<{permissions}|Object>} returns the permissions required to access.
*/
exports.getRequiredResourceRole = async (
permLevel,
Expand Down
6 changes: 3 additions & 3 deletions packages/server/src/api/controllers/permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
getDBRoleID,
getExternalRoleID,
getBuiltinRoles,
checkForRoleResourceArray,
} = require("@budibase/backend-core/roles")
const { getRoleParams } = require("../../db/utils")
const {
Expand Down Expand Up @@ -144,12 +145,11 @@ exports.getResourcePerms = async function (ctx) {
for (let level of SUPPORTED_LEVELS) {
// update the various roleIds in the resource permissions
for (let role of roles) {
const rolePerms = role.permissions
const rolePerms = checkForRoleResourceArray(role.permissions, resourceId)
if (
rolePerms &&
rolePerms[resourceId] &&
(rolePerms[resourceId] === level ||
rolePerms[resourceId].indexOf(level) !== -1)
rolePerms[resourceId].indexOf(level) !== -1
) {
permissions[level] = getExternalRoleID(role._id)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/api/routes/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ router
.get(
"/api/tables/:tableId",
paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
authorized(PermissionTypes.TABLE, PermissionLevels.READ, { schema: true }),
tableController.find
)
/**
Expand Down
32 changes: 26 additions & 6 deletions packages/server/src/middleware/authorized.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
} = require("@budibase/backend-core/roles")
const {
PermissionTypes,
PermissionLevels,
doesHaveBasePermission,
} = require("@budibase/backend-core/permissions")
const builderMiddleware = require("./builder")
Expand Down Expand Up @@ -64,7 +65,7 @@ const checkAuthorizedResource = async (
}

module.exports =
(permType, permLevel = null) =>
(permType, permLevel = null, opts = { schema: false }) =>
async (ctx, next) => {
// webhooks don't need authentication, each webhook unique
// also internal requests (between services) don't need authorized
Expand All @@ -81,15 +82,25 @@ module.exports =
await builderMiddleware(ctx, permType)

// get the resource roles
let resourceRoles = []
let resourceRoles = [],
otherLevelRoles
const otherLevel =
permLevel === PermissionLevels.READ
? PermissionLevels.WRITE
: PermissionLevels.READ
const appId = getAppId()
if (appId && hasResource(ctx)) {
resourceRoles = await getRequiredResourceRole(permLevel, ctx)
if (opts && opts.schema) {
otherLevelRoles = await getRequiredResourceRole(otherLevel, ctx)
}
}

// if the resource is public, proceed
const isPublicResource = resourceRoles.includes(BUILTIN_ROLE_IDS.PUBLIC)
if (isPublicResource) {
if (
resourceRoles.includes(BUILTIN_ROLE_IDS.PUBLIC) ||
(otherLevelRoles && otherLevelRoles.includes(BUILTIN_ROLE_IDS.PUBLIC))
) {
return next()
}

Expand All @@ -98,8 +109,17 @@ module.exports =
return ctx.throw(403, "Session not authenticated")
}

// check authorized
await checkAuthorized(ctx, resourceRoles, permType, permLevel)
try {
// check authorized
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
return csrf(ctx, next)
Expand Down