Merge pull request #3388 from Budibase/fix/roles-fix
Fixing issue with RBAC always defaulting to base permissions
This commit is contained in:
commit
b3515f2fd6
|
@ -131,38 +131,14 @@ exports.getBuiltinPermissionByID = id => {
|
||||||
return perms.find(perm => perm._id === id)
|
return perms.find(perm => perm._id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.doesHaveResourcePermission = (
|
exports.doesHaveBasePermission = (permType, permLevel, rolesHierarchy) => {
|
||||||
permissions,
|
const basePermissions = [
|
||||||
permLevel,
|
...new Set(rolesHierarchy.map(role => role.permissionId)),
|
||||||
{ resourceId, subResourceId }
|
]
|
||||||
) => {
|
|
||||||
// set foundSub to not subResourceId, incase there is no subResource
|
|
||||||
let foundMain = false,
|
|
||||||
foundSub = false
|
|
||||||
for (let [resource, levels] of Object.entries(permissions)) {
|
|
||||||
if (resource === resourceId && levels.indexOf(permLevel) !== -1) {
|
|
||||||
foundMain = true
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
subResourceId &&
|
|
||||||
resource === subResourceId &&
|
|
||||||
levels.indexOf(permLevel) !== -1
|
|
||||||
) {
|
|
||||||
foundSub = true
|
|
||||||
}
|
|
||||||
// this will escape if foundMain only when no sub resource
|
|
||||||
if (foundMain && foundSub) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return foundMain || foundSub
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => {
|
|
||||||
const builtins = Object.values(BUILTIN_PERMISSIONS)
|
const builtins = Object.values(BUILTIN_PERMISSIONS)
|
||||||
let permissions = flatten(
|
let permissions = flatten(
|
||||||
builtins
|
builtins
|
||||||
.filter(builtin => permissionIds.indexOf(builtin._id) !== -1)
|
.filter(builtin => basePermissions.indexOf(builtin._id) !== -1)
|
||||||
.map(builtin => builtin.permissions)
|
.map(builtin => builtin.permissions)
|
||||||
)
|
)
|
||||||
for (let permission of permissions) {
|
for (let permission of permissions) {
|
||||||
|
|
|
@ -170,47 +170,18 @@ async function getAllUserRoles(appId, userRoleId) {
|
||||||
* to determine if a user can access something that requires a specific role.
|
* to determine if a user can access something that requires a specific role.
|
||||||
* @param {string} appId The ID of the application from which roles should be obtained.
|
* @param {string} appId The ID of the application from which roles should be obtained.
|
||||||
* @param {string} userRoleId The user's role ID, this can be found in their access token.
|
* @param {string} userRoleId The user's role ID, this can be found in their access token.
|
||||||
|
* @param {object} opts Various options, such as whether to only retrieve the IDs (default true).
|
||||||
* @returns {Promise<string[]>} returns an ordered array of the roles, with the first being their
|
* @returns {Promise<string[]>} returns an ordered array of the roles, with the first being their
|
||||||
* highest level of access and the last being the lowest level.
|
* highest level of access and the last being the lowest level.
|
||||||
*/
|
*/
|
||||||
exports.getUserRoleHierarchy = async (appId, userRoleId) => {
|
exports.getUserRoleHierarchy = async (
|
||||||
|
appId,
|
||||||
|
userRoleId,
|
||||||
|
opts = { idOnly: true }
|
||||||
|
) => {
|
||||||
// special case, if they don't have a role then they are a public user
|
// special case, if they don't have a role then they are a public user
|
||||||
return (await getAllUserRoles(appId, userRoleId)).map(role => role._id)
|
const roles = await getAllUserRoles(appId, userRoleId)
|
||||||
}
|
return opts.idOnly ? roles.map(role => role._id) : roles
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all of the user permissions which could be found across the role hierarchy
|
|
||||||
* @param appId The ID of the application from which roles should be obtained.
|
|
||||||
* @param userRoleId The user's role ID, this can be found in their access token.
|
|
||||||
* @returns {Promise<{basePermissions: string[], permissions: Object}>} the base
|
|
||||||
* permission IDs as well as any custom resource permissions.
|
|
||||||
*/
|
|
||||||
exports.getUserPermissions = async (appId, userRoleId) => {
|
|
||||||
const rolesHierarchy = await getAllUserRoles(appId, userRoleId)
|
|
||||||
const basePermissions = [
|
|
||||||
...new Set(rolesHierarchy.map(role => role.permissionId)),
|
|
||||||
]
|
|
||||||
const permissions = {}
|
|
||||||
for (let role of rolesHierarchy) {
|
|
||||||
if (role.permissions) {
|
|
||||||
for (let [resource, levels] of Object.entries(role.permissions)) {
|
|
||||||
if (!permissions[resource]) {
|
|
||||||
permissions[resource] = []
|
|
||||||
}
|
|
||||||
const permsSet = new Set(permissions[resource])
|
|
||||||
if (Array.isArray(levels)) {
|
|
||||||
levels.forEach(level => permsSet.add(level))
|
|
||||||
} else {
|
|
||||||
permsSet.add(levels)
|
|
||||||
}
|
|
||||||
permissions[resource] = [...permsSet]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
basePermissions,
|
|
||||||
permissions,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -246,6 +217,39 @@ exports.getAllRoles = async appId => {
|
||||||
return roles
|
return roles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This retrieves the required role/
|
||||||
|
* @param appId
|
||||||
|
* @param permLevel
|
||||||
|
* @param resourceId
|
||||||
|
* @param subResourceId
|
||||||
|
* @return {Promise<{permissions}|Object>}
|
||||||
|
*/
|
||||||
|
exports.getRequiredResourceRole = async (
|
||||||
|
appId,
|
||||||
|
permLevel,
|
||||||
|
{ resourceId, subResourceId }
|
||||||
|
) => {
|
||||||
|
const roles = await exports.getAllRoles(appId)
|
||||||
|
let main = [],
|
||||||
|
sub = []
|
||||||
|
for (let role of roles) {
|
||||||
|
// no permissions, ignore it
|
||||||
|
if (!role.permissions) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const mainRes = role.permissions[resourceId]
|
||||||
|
const subRes = role.permissions[subResourceId]
|
||||||
|
if (mainRes && mainRes.indexOf(permLevel) !== -1) {
|
||||||
|
main.push(role._id)
|
||||||
|
} else if (subRes && subRes.indexOf(permLevel) !== -1) {
|
||||||
|
sub.push(role._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// for now just return the IDs
|
||||||
|
return main.concat(sub)
|
||||||
|
}
|
||||||
|
|
||||||
class AccessController {
|
class AccessController {
|
||||||
constructor(appId) {
|
constructor(appId) {
|
||||||
this.appId = appId
|
this.appId = appId
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
const { getUserPermissions } = require("@budibase/auth/roles")
|
const {
|
||||||
|
getUserRoleHierarchy,
|
||||||
|
getRequiredResourceRole,
|
||||||
|
BUILTIN_ROLE_IDS,
|
||||||
|
} = require("@budibase/auth/roles")
|
||||||
const {
|
const {
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
doesHaveResourcePermission,
|
|
||||||
doesHaveBasePermission,
|
doesHaveBasePermission,
|
||||||
} = require("@budibase/auth/permissions")
|
} = require("@budibase/auth/permissions")
|
||||||
const builderMiddleware = require("./builder")
|
const builderMiddleware = require("./builder")
|
||||||
|
@ -28,13 +31,7 @@ module.exports =
|
||||||
await builderMiddleware(ctx, permType)
|
await builderMiddleware(ctx, permType)
|
||||||
|
|
||||||
const isAuthed = ctx.isAuthenticated
|
const isAuthed = ctx.isAuthenticated
|
||||||
const { basePermissions, permissions } = await getUserPermissions(
|
|
||||||
ctx.appId,
|
|
||||||
ctx.roleId
|
|
||||||
)
|
|
||||||
|
|
||||||
// builders for now have permission to do anything
|
// builders for now have permission to do anything
|
||||||
// TODO: in future should consider separating permissions with an require("@budibase/auth").isClient check
|
|
||||||
let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
|
let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
|
||||||
const isBuilderApi = permType === PermissionTypes.BUILDER
|
const isBuilderApi = permType === PermissionTypes.BUILDER
|
||||||
if (isBuilder) {
|
if (isBuilder) {
|
||||||
|
@ -43,20 +40,30 @@ module.exports =
|
||||||
return ctx.throw(403, "Not Authorized")
|
return ctx.throw(403, "Not Authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
// need to check this first, in-case public access, don't check authed until last
|
||||||
hasResource(ctx) &&
|
const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC
|
||||||
doesHaveResourcePermission(permissions, permLevel, ctx)
|
const hierarchy = await getUserRoleHierarchy(ctx.appId, roleId, {
|
||||||
) {
|
idOnly: false,
|
||||||
return next()
|
})
|
||||||
|
const permError = "User does not have permission"
|
||||||
|
let possibleRoleIds = []
|
||||||
|
if (hasResource(ctx)) {
|
||||||
|
possibleRoleIds = await getRequiredResourceRole(ctx.appId, permLevel, ctx)
|
||||||
|
}
|
||||||
|
// check if we found a role, if not fallback to base permissions
|
||||||
|
if (possibleRoleIds.length > 0) {
|
||||||
|
const found = hierarchy.find(
|
||||||
|
role => possibleRoleIds.indexOf(role._id) !== -1
|
||||||
|
)
|
||||||
|
return found ? next() : ctx.throw(403, permError)
|
||||||
|
} else if (!doesHaveBasePermission(permType, permLevel, hierarchy)) {
|
||||||
|
ctx.throw(403, permError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if they are not authed, then anything using the authorized middleware will fail
|
||||||
if (!isAuthed) {
|
if (!isAuthed) {
|
||||||
ctx.throw(403, "Session not authenticated")
|
ctx.throw(403, "Session not authenticated")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doesHaveBasePermission(permType, permLevel, basePermissions)) {
|
|
||||||
ctx.throw(403, "User does not have permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ jest.mock("../../environment", () => ({
|
||||||
const authorizedMiddleware = require("../authorized")
|
const authorizedMiddleware = require("../authorized")
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const { PermissionTypes, PermissionLevels } = require("@budibase/auth/permissions")
|
const { PermissionTypes, PermissionLevels } = require("@budibase/auth/permissions")
|
||||||
|
require("@budibase/auth").init(require("../../db"))
|
||||||
|
|
||||||
class TestConfiguration {
|
class TestConfiguration {
|
||||||
constructor(role) {
|
constructor(role) {
|
||||||
|
@ -21,6 +22,7 @@ class TestConfiguration {
|
||||||
request: {
|
request: {
|
||||||
url: ""
|
url: ""
|
||||||
},
|
},
|
||||||
|
appId: "",
|
||||||
auth: {},
|
auth: {},
|
||||||
next: this.next,
|
next: this.next,
|
||||||
throw: this.throw
|
throw: this.throw
|
||||||
|
|
Loading…
Reference in New Issue