Merge pull request #14844 from Budibase/fix/multi-inheritance-preview-role

Preview role - multi-inheritance fixes
This commit is contained in:
Michael Drury 2024-10-22 16:48:14 +01:00 committed by GitHub
commit 9575095da3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 55 deletions

View File

@ -213,6 +213,22 @@ export function getBuiltinRole(roleId: string): Role | undefined {
return cloneDeep(role) return cloneDeep(role)
} }
export function validInherits(
allRoles: RoleDoc[],
inherits?: string | string[]
): boolean {
if (!inherits) {
return false
}
const find = (id: string) => allRoles.find(r => roleIDsAreEqual(r._id!, id))
if (Array.isArray(inherits)) {
const filtered = inherits.filter(roleId => find(roleId))
return inherits.length !== 0 && filtered.length === inherits.length
} else {
return !!find(inherits)
}
}
/** /**
* Works through the inheritance ranks to see how far up the builtin stack this ID is. * Works through the inheritance ranks to see how far up the builtin stack this ID is.
*/ */
@ -290,7 +306,7 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
: roleId1 : roleId1
} }
export function compareRoleIds(roleId1: string, roleId2: string) { export function roleIDsAreEqual(roleId1: string, roleId2: string) {
// make sure both role IDs are prefixed correctly // make sure both role IDs are prefixed correctly
return prefixRoleID(roleId1) === prefixRoleID(roleId2) return prefixRoleID(roleId1) === prefixRoleID(roleId2)
} }
@ -323,7 +339,7 @@ export function findRole(
roleId = prefixRoleID(roleId) roleId = prefixRoleID(roleId)
} }
const dbRole = roles.find( const dbRole = roles.find(
role => role._id && compareRoleIds(role._id, roleId) role => role._id && roleIDsAreEqual(role._id, roleId)
) )
if (!dbRole && !isBuiltin(roleId) && opts?.defaultPublic) { if (!dbRole && !isBuiltin(roleId) && opts?.defaultPublic) {
return cloneDeep(BUILTIN_ROLES.PUBLIC) return cloneDeep(BUILTIN_ROLES.PUBLIC)
@ -557,7 +573,7 @@ export class AccessController {
} }
return ( return (
roleIds?.find(roleId => compareRoleIds(roleId, tryingRoleId)) !== roleIds?.find(roleId => roleIDsAreEqual(roleId, tryingRoleId)) !==
undefined undefined
) )
} }

View File

@ -18,6 +18,7 @@ import {
UserCtx, UserCtx,
UserMetadata, UserMetadata,
DocumentType, DocumentType,
PermissionLevel,
} from "@budibase/types" } from "@budibase/types"
import { RoleColor, sdk as sharedSdk, helpers } from "@budibase/shared-core" import { RoleColor, sdk as sharedSdk, helpers } from "@budibase/shared-core"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -35,11 +36,11 @@ async function removeRoleFromOthers(roleId: string) {
let changed = false let changed = false
if (Array.isArray(role.inherits)) { if (Array.isArray(role.inherits)) {
const newInherits = role.inherits.filter( const newInherits = role.inherits.filter(
id => !roles.compareRoleIds(id, roleId) id => !roles.roleIDsAreEqual(id, roleId)
) )
changed = role.inherits.length !== newInherits.length changed = role.inherits.length !== newInherits.length
role.inherits = newInherits role.inherits = newInherits
} else if (role.inherits && roles.compareRoleIds(role.inherits, roleId)) { } else if (role.inherits && roles.roleIDsAreEqual(role.inherits, roleId)) {
role.inherits = roles.BUILTIN_ROLE_IDS.PUBLIC role.inherits = roles.BUILTIN_ROLE_IDS.PUBLIC
changed = true changed = true
} }
@ -125,6 +126,17 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
ctx.throw(400, "Cannot change custom role name") ctx.throw(400, "Cannot change custom role name")
} }
// custom roles should always inherit basic - if they don't inherit anything else
if (!inherits && roles.validInherits(allRoles, dbRole?.inherits)) {
inherits = dbRole?.inherits
} else if (!roles.validInherits(allRoles, inherits)) {
inherits = [roles.BUILTIN_ROLE_IDS.BASIC]
}
// assume write permission level for newly created roles
if (isCreate && !permissionId) {
permissionId = PermissionLevel.WRITE
}
const role = new roles.Role(_id, name, permissionId, { const role = new roles.Role(_id, name, permissionId, {
displayName: uiMetadata?.displayName || name, displayName: uiMetadata?.displayName || name,
description: uiMetadata?.description || "Custom role", description: uiMetadata?.description || "Custom role",
@ -226,35 +238,23 @@ export async function accessible(ctx: UserCtx<void, AccessibleRolesResponse>) {
if (!roleId) { if (!roleId) {
roleId = roles.BUILTIN_ROLE_IDS.PUBLIC roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
} }
// If a custom role is provided in the header, filter out higher level roles
const roleHeader = ctx.header[Header.PREVIEW_ROLE]
if (Array.isArray(roleHeader)) {
ctx.throw(400, `Too many roles specified in ${Header.PREVIEW_ROLE} header`)
}
const isBuilder = ctx.user && sharedSdk.users.isAdminOrBuilder(ctx.user)
let roleIds: string[] = [] let roleIds: string[] = []
if (ctx.user && sharedSdk.users.isAdminOrBuilder(ctx.user)) { if (!roleHeader && isBuilder) {
const appId = context.getAppId() const appId = context.getAppId()
if (appId) { if (appId) {
roleIds = await roles.getAllRoleIds(appId) roleIds = await roles.getAllRoleIds(appId)
} }
} else if (isBuilder && roleHeader) {
roleIds = await roles.getUserRoleIdHierarchy(roleHeader)
} else { } else {
roleIds = await roles.getUserRoleIdHierarchy(roleId!) roleIds = await roles.getUserRoleIdHierarchy(roleId!)
} }
// If a custom role is provided in the header, filter out higher level roles
const roleHeader = ctx.header?.[Header.PREVIEW_ROLE] as string
if (roleHeader && !Object.keys(roles.BUILTIN_ROLE_IDS).includes(roleHeader)) {
const role = await roles.getRole(roleHeader)
const inherits = role?.inherits
const orderedRoles = roleIds.reverse()
let filteredRoles = [roleHeader]
for (let role of orderedRoles) {
filteredRoles = [role, ...filteredRoles]
if (
(Array.isArray(inherits) && inherits.includes(role)) ||
role === inherits
) {
break
}
}
filteredRoles.pop()
roleIds = [roleHeader, ...filteredRoles]
}
ctx.body = roleIds.map(roleId => roles.getExternalRoleID(roleId)) ctx.body = roleIds.map(roleId => roles.getExternalRoleID(roleId))
} }

View File

@ -38,6 +38,26 @@ describe("/roles", () => {
_id: dbCore.prefixRoleID(res._id!), _id: dbCore.prefixRoleID(res._id!),
}) })
}) })
it("handle a role with invalid inherits", async () => {
const role = basicRole()
role.inherits = ["not_real", "some_other_not_real"]
const res = await config.api.roles.save(role, {
status: 200,
})
expect(res.inherits).toEqual([BUILTIN_ROLE_IDS.BASIC])
})
it("handle a role with no inherits", async () => {
const role = basicRole()
role.inherits = []
const res = await config.api.roles.save(role, {
status: 200,
})
expect(res.inherits).toEqual([BUILTIN_ROLE_IDS.BASIC])
})
}) })
describe("update", () => { describe("update", () => {
@ -149,6 +169,17 @@ describe("/roles", () => {
{ status: 400, body: { message: LOOP_ERROR } } { status: 400, body: { message: LOOP_ERROR } }
) )
}) })
it("handle updating a role, without its inherits", async () => {
const res = await config.api.roles.save({
...basicRole(),
inherits: [BUILTIN_ROLE_IDS.ADMIN],
})
// remove the roles so that it will default back to DB roles, then save again
delete res.inherits
const updatedRes = await config.api.roles.save(res)
expect(updatedRes.inherits).toEqual([BUILTIN_ROLE_IDS.ADMIN])
})
}) })
describe("fetch", () => { describe("fetch", () => {
@ -298,6 +329,23 @@ describe("/roles", () => {
} }
) )
}) })
it("should fetch preview role correctly even without basic specified", async () => {
const role = await config.api.roles.save(basicRole())
// have to forcefully delete the inherits from DB - technically can't
// happen anymore - but good test case
await dbCore.getDB(config.appId!).put({
...role,
_id: dbCore.prefixRoleID(role._id!),
inherits: [],
})
await config.withHeaders({ "x-budibase-role": role.name }, async () => {
const res = await config.api.roles.accessible({
status: 200,
})
expect(res).toEqual([role.name])
})
})
}) })
describe("accessible - multi-inheritance", () => { describe("accessible - multi-inheritance", () => {

View File

@ -86,7 +86,6 @@ describe("/screens", () => {
status: 200, status: 200,
} }
) )
// basic and role1 screen
expect(res.screens.length).toEqual(screenIds.length) expect(res.screens.length).toEqual(screenIds.length)
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort()) expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
}) })
@ -107,6 +106,25 @@ describe("/screens", () => {
screen2._id!, screen2._id!,
]) ])
}) })
it("should be able to fetch basic and screen 1 with role1 in role header", async () => {
await config.withHeaders(
{
"x-budibase-role": role1._id!,
},
async () => {
const res = await config.api.application.getDefinition(
config.prodAppId!,
{
status: 200,
}
)
const screenIds = [screen._id!, screen1._id!]
expect(res.screens.length).toEqual(screenIds.length)
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
}
)
})
}) })
describe("save", () => { describe("save", () => {

View File

@ -56,22 +56,9 @@ export default async (ctx: UserCtx, next: any) => {
ctx.request && ctx.request &&
(ctx.request.headers[constants.Header.PREVIEW_ROLE] as string) (ctx.request.headers[constants.Header.PREVIEW_ROLE] as string)
if (isBuilder && isDevApp && roleHeader) { if (isBuilder && isDevApp && roleHeader) {
// Ensure the role is valid by ensuring a definition exists roleId = roleHeader
try { // Delete admin and builder flags so that the specified role is honoured
if (roleHeader) { ctx.user = users.removePortalUserPermissions(ctx.user) as ContextUser
const role = await roles.getRole(roleHeader)
if (role) {
roleId = roleHeader
// Delete admin and builder flags so that the specified role is honoured
ctx.user = users.removePortalUserPermissions(
ctx.user
) as ContextUser
}
}
} catch (error) {
// Swallow error and do nothing
}
} }
} }

View File

@ -8,31 +8,31 @@ import {
} from "../../automations" } from "../../automations"
import { import {
AIOperationEnum, AIOperationEnum,
AutoFieldSubType,
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationEventType,
AutomationResults, AutomationResults,
AutomationStatus, AutomationStatus,
AutomationStep, AutomationStep,
AutomationStepType, AutomationStepType,
AutomationTrigger, AutomationTrigger,
AutomationTriggerStepId, AutomationTriggerStepId,
BBReferenceFieldSubType,
CreateViewRequest,
Datasource, Datasource,
FieldSchema,
FieldType, FieldType,
INTERNAL_TABLE_SOURCE_ID,
JsonFieldSubType,
LoopStepType,
Query,
Role,
SourceName, SourceName,
Table, Table,
INTERNAL_TABLE_SOURCE_ID,
TableSourceType, TableSourceType,
Query,
Webhook, Webhook,
WebhookActionType, WebhookActionType,
AutomationEventType,
LoopStepType,
FieldSchema,
BBReferenceFieldSubType,
JsonFieldSubType,
AutoFieldSubType,
Role,
CreateViewRequest,
} from "@budibase/types" } from "@budibase/types"
import { LoopInput } from "../../definitions/automations" import { LoopInput } from "../../definitions/automations"
import { merge } from "lodash" import { merge } from "lodash"
@ -439,7 +439,7 @@ export function updateRowAutomationWithFilters(
appId: string, appId: string,
tableId: string tableId: string
): Automation { ): Automation {
const automation: Automation = { return {
name: "updateRowWithFilters", name: "updateRowWithFilters",
type: "automation", type: "automation",
appId, appId,
@ -472,7 +472,6 @@ export function updateRowAutomationWithFilters(
}, },
}, },
} }
return automation
} }
export function basicAutomationResults( export function basicAutomationResults(