diff --git a/packages/shared-core/src/helpers/roles.ts b/packages/shared-core/src/helpers/roles.ts new file mode 100644 index 0000000000..27f28cff82 --- /dev/null +++ b/packages/shared-core/src/helpers/roles.ts @@ -0,0 +1,47 @@ +import { Role } from "@budibase/types" + +// Function to detect loops in roles +export function checkForRoleInheritanceLoops(roles: Role[]): boolean { + const roleMap = new Map() + roles.forEach(role => { + roleMap.set(role._id!, role) + }) + + const checked = new Set() + const checking = new Set() + + function hasLoop(roleId: string): boolean { + if (checking.has(roleId)) { + return true + } + if (checked.has(roleId)) { + return false + } + + checking.add(roleId) + + const role = roleMap.get(roleId) + if (!role) { + // role not found - ignore + checking.delete(roleId) + return false + } + + const inherits = Array.isArray(role.inherits) + ? role.inherits + : [role.inherits] + for (const inheritedId of inherits) { + if (inheritedId && hasLoop(inheritedId)) { + return true + } + } + + // mark this role has been fully checked + checking.delete(roleId) + checked.add(roleId) + + return false + } + + return !!roles.find(role => hasLoop(role._id!)) +} diff --git a/packages/shared-core/src/helpers/tests/roles.spec.ts b/packages/shared-core/src/helpers/tests/roles.spec.ts new file mode 100644 index 0000000000..7e52ded3f6 --- /dev/null +++ b/packages/shared-core/src/helpers/tests/roles.spec.ts @@ -0,0 +1,61 @@ +import { checkForRoleInheritanceLoops } from "../roles" +import { Role } from "@budibase/types" + +/** + * This unit test exists as this utility will be used in the frontend and backend, confirmation + * of its API and expected results is useful since the backend tests won't confirm it works + * exactly as the frontend needs it to - easy to add specific test cases here that the frontend + * might need to check/cover. + */ + +interface TestRole extends Omit { + _id: string +} + +let allRoles: TestRole[] = [] + +function role(id: string, inherits: string | string[]): TestRole { + const role = { + _id: id, + inherits: inherits, + name: "ROLE", + permissionId: "PERMISSION", + permissions: {}, // not needed for this test + } + allRoles.push(role) + return role +} + +describe("role utilities", () => { + let role1: TestRole, role2: TestRole + + beforeEach(() => { + role1 = role("role_1", []) + role2 = role("role_2", [role1._id]) + }) + + afterEach(() => { + allRoles = [] + }) + + function check(hasLoop: boolean) { + const result = checkForRoleInheritanceLoops(allRoles) + expect(result).toBe(hasLoop) + } + + describe("checkForRoleInheritanceLoops", () => { + it("should confirm no loops", () => { + check(false) + }) + + it("should confirm there is a loop", () => { + const role3 = role("role_3", [role2._id]) + const role4 = role("role_4", [role3._id, role2._id, role1._id]) + role3.inherits = [ + ...(Array.isArray(role3.inherits) ? role3.inherits : []), + role4._id, + ] + check(true) + }) + }) +})