From 0c8228edad3b1a288c5232e891ed0745f4774a53 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 5 Aug 2024 15:45:49 +0100 Subject: [PATCH 1/4] Initial work - some re-typing and updating the role tests to typescript - using role test API to make this a bit easier to adjust going forward. --- packages/backend-core/src/security/roles.ts | 42 +++- packages/server/src/api/controllers/role.ts | 5 +- .../server/src/api/routes/tests/role.spec.js | 182 ------------------ .../server/src/api/routes/tests/role.spec.ts | 164 ++++++++++++++++ .../src/tests/utilities/TestConfiguration.ts | 1 + .../server/src/tests/utilities/api/role.ts | 11 +- .../server/src/tests/utilities/structures.ts | 4 +- packages/types/src/api/web/role.ts | 4 +- packages/types/src/documents/app/role.ts | 2 +- packages/types/src/sdk/events/role.ts | 6 +- 10 files changed, 218 insertions(+), 203 deletions(-) delete mode 100644 packages/server/src/api/routes/tests/role.spec.js create mode 100644 packages/server/src/api/routes/tests/role.spec.ts diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index a64be6b319..61d9786d32 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -42,7 +42,7 @@ export class Role implements RoleDoc { _rev?: string name: string permissionId: string - inherits?: string + inherits?: string | string[] version?: string permissions = {} @@ -54,8 +54,10 @@ export class Role implements RoleDoc { this.version = RoleIDVersion.NAME } - addInheritance(inherits: string) { - this.inherits = inherits + addInheritance(inherits?: string | string[]) { + if (inherits) { + this.inherits = inherits + } return this } } @@ -113,7 +115,11 @@ export function builtinRoleToNumber(id: string) { if (!role) { break } - role = builtins[role.inherits!] + if (Array.isArray(role.inherits)) { + // TODO: role inheritance + } else { + role = builtins[role.inherits!] + } count++ } while (role !== null) return count @@ -130,7 +136,12 @@ export async function roleToNumber(id: string) { defaultPublic: true, })) as RoleDoc[] for (let role of hierarchy) { - if (role?.inherits && isBuiltin(role.inherits)) { + if (!role.inherits) { + continue + } + if (Array.isArray(role.inherits)) { + // TODO: role inheritance + } else if (isBuiltin(role.inherits)) { return builtinRoleToNumber(role.inherits) + 1 } } @@ -202,16 +213,27 @@ async function getAllUserRoles( let currentRole = await getRole(userRoleId, opts) let roles = currentRole ? [currentRole] : [] let roleIds = [userRoleId] + const rolesFound = (ids: string | string[]) => { + if (Array.isArray(ids)) { + return ids.filter(id => roleIds.includes(id)).length === ids.length + } else { + return roleIds.includes(ids) + } + } // get all the inherited roles while ( currentRole && currentRole.inherits && - roleIds.indexOf(currentRole.inherits) === -1 + !rolesFound(currentRole.inherits) ) { - roleIds.push(currentRole.inherits) - currentRole = await getRole(currentRole.inherits) - if (currentRole) { - roles.push(currentRole) + if (Array.isArray(currentRole.inherits)) { + // TODO: role inheritance + } else { + roleIds.push(currentRole.inherits) + currentRole = await getRole(currentRole.inherits) + if (currentRole) { + roles.push(currentRole) + } } } return roles diff --git a/packages/server/src/api/controllers/role.ts b/packages/server/src/api/controllers/role.ts index 3398c8102c..3431724d7d 100644 --- a/packages/server/src/api/controllers/role.ts +++ b/packages/server/src/api/controllers/role.ts @@ -182,7 +182,10 @@ export async function accessible(ctx: UserCtx) { let filteredRoles = [roleHeader] for (let role of orderedRoles) { filteredRoles = [role, ...filteredRoles] - if (role === inherits) { + if ( + (Array.isArray(inherits) && inherits.includes(role)) || + role === inherits + ) { break } } diff --git a/packages/server/src/api/routes/tests/role.spec.js b/packages/server/src/api/routes/tests/role.spec.js deleted file mode 100644 index 4575f9b213..0000000000 --- a/packages/server/src/api/routes/tests/role.spec.js +++ /dev/null @@ -1,182 +0,0 @@ -const { roles, events, permissions } = require("@budibase/backend-core") -const setup = require("./utilities") -const { PermissionLevel } = require("@budibase/types") -const { basicRole } = setup.structures -const { BUILTIN_ROLE_IDS } = roles -const { BuiltinPermissionID } = permissions - -describe("/roles", () => { - let request = setup.getRequest() - let config = setup.getConfig() - - afterAll(setup.afterAll) - - beforeAll(async () => { - await config.init() - }) - - const createRole = async role => { - if (!role) { - role = basicRole() - } - - return request - .post(`/api/roles`) - .send(role) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - } - - describe("create", () => { - it("returns a success message when role is successfully created", async () => { - const role = basicRole() - const res = await createRole(role) - - expect(res.body._id).toBeDefined() - expect(res.body._rev).toBeDefined() - expect(events.role.updated).not.toBeCalled() - expect(events.role.created).toBeCalledTimes(1) - expect(events.role.created).toBeCalledWith(res.body) - }) - }) - - describe("update", () => { - it("updates a role", async () => { - const role = basicRole() - let res = await createRole(role) - jest.clearAllMocks() - res = await createRole(res.body) - - expect(res.body._id).toBeDefined() - expect(res.body._rev).toBeDefined() - expect(events.role.created).not.toBeCalled() - expect(events.role.updated).toBeCalledTimes(1) - expect(events.role.updated).toBeCalledWith(res.body) - }) - }) - - describe("fetch", () => { - beforeAll(async () => { - // Recreate the app - await config.init() - }) - - it("should list custom roles, plus 2 default roles", async () => { - const customRole = await config.createRole() - - const res = await request - .get(`/api/roles`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - - expect(res.body.length).toBe(5) - - const adminRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN) - expect(adminRole).toBeDefined() - expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER) - expect(adminRole.permissionId).toEqual(BuiltinPermissionID.ADMIN) - - const powerUserRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.POWER) - expect(powerUserRole).toBeDefined() - expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC) - expect(powerUserRole.permissionId).toEqual(BuiltinPermissionID.POWER) - - const customRoleFetched = res.body.find(r => r._id === customRole.name) - expect(customRoleFetched).toBeDefined() - expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC) - expect(customRoleFetched.permissionId).toEqual( - BuiltinPermissionID.READ_ONLY - ) - }) - - it("should be able to get the role with a permission added", async () => { - const table = await config.createTable() - await config.api.permission.add({ - roleId: BUILTIN_ROLE_IDS.POWER, - resourceId: table._id, - level: PermissionLevel.READ, - }) - const res = await request - .get(`/api/roles`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.length).toBeGreaterThan(0) - const power = res.body.find(role => role._id === BUILTIN_ROLE_IDS.POWER) - expect(power.permissions[table._id]).toEqual(["read"]) - }) - }) - - describe("destroy", () => { - it("should delete custom roles", async () => { - const customRole = await config.createRole({ - name: "user", - permissionId: BuiltinPermissionID.READ_ONLY, - inherits: BUILTIN_ROLE_IDS.BASIC, - }) - delete customRole._rev_tree - await request - .delete(`/api/roles/${customRole._id}/${customRole._rev}`) - .set(config.defaultHeaders()) - .expect(200) - await request - .get(`/api/roles/${customRole._id}`) - .set(config.defaultHeaders()) - .expect(404) - expect(events.role.deleted).toBeCalledTimes(1) - expect(events.role.deleted).toBeCalledWith(customRole) - }) - }) - - describe("accessible", () => { - it("should be able to fetch accessible roles (with builder)", async () => { - const res = await request - .get("/api/roles/accessible") - .set(config.defaultHeaders()) - .expect(200) - expect(res.body.length).toBe(5) - expect(typeof res.body[0]).toBe("string") - }) - - it("should be able to fetch accessible roles (basic user)", async () => { - const res = await request - .get("/api/roles/accessible") - .set(await config.basicRoleHeaders()) - .expect(200) - expect(res.body.length).toBe(2) - expect(res.body[0]).toBe("BASIC") - expect(res.body[1]).toBe("PUBLIC") - }) - - it("should be able to fetch accessible roles (no user)", async () => { - const res = await request - .get("/api/roles/accessible") - .set(config.publicHeaders()) - .expect(200) - expect(res.body.length).toBe(1) - expect(res.body[0]).toBe("PUBLIC") - }) - - it("should not fetch higher level accessible roles when a custom role header is provided", async () => { - await createRole({ - name: `CUSTOM_ROLE`, - inherits: roles.BUILTIN_ROLE_IDS.BASIC, - permissionId: permissions.BuiltinPermissionID.READ_ONLY, - version: "name", - }) - const res = await request - .get("/api/roles/accessible") - .set({ - ...config.defaultHeaders(), - "x-budibase-role": "CUSTOM_ROLE", - }) - .expect(200) - expect(res.body.length).toBe(3) - expect(res.body[0]).toBe("CUSTOM_ROLE") - expect(res.body[1]).toBe("BASIC") - expect(res.body[2]).toBe("PUBLIC") - }) - }) -}) diff --git a/packages/server/src/api/routes/tests/role.spec.ts b/packages/server/src/api/routes/tests/role.spec.ts new file mode 100644 index 0000000000..127be789b9 --- /dev/null +++ b/packages/server/src/api/routes/tests/role.spec.ts @@ -0,0 +1,164 @@ +import { roles, events, permissions } from "@budibase/backend-core" +import * as setup from "./utilities" +import { PermissionLevel } from "@budibase/types" + +const { basicRole } = setup.structures +const { BUILTIN_ROLE_IDS } = roles +const { BuiltinPermissionID } = permissions + +describe("/roles", () => { + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeAll(async () => { + await config.init() + }) + + describe("create", () => { + it("returns a success message when role is successfully created", async () => { + const role = basicRole() + const res = await config.api.roles.save(role, { + status: 200, + }) + + expect(res._id).toBeDefined() + expect(res._rev).toBeDefined() + expect(events.role.updated).not.toHaveBeenCalled() + expect(events.role.created).toHaveBeenCalledTimes(1) + expect(events.role.created).toHaveBeenCalledWith(res) + }) + }) + + describe("update", () => { + it("updates a role", async () => { + const role = basicRole() + let res = await config.api.roles.save(role, { + status: 200, + }) + jest.clearAllMocks() + res = await config.api.roles.save(res, { + status: 200, + }) + + expect(res._id).toBeDefined() + expect(res._rev).toBeDefined() + expect(events.role.created).not.toHaveBeenCalled() + expect(events.role.updated).toHaveBeenCalledTimes(1) + expect(events.role.updated).toHaveBeenCalledWith(res) + }) + }) + + describe("fetch", () => { + beforeAll(async () => { + // Recreate the app + await config.init() + }) + + it("should list custom roles, plus 2 default roles", async () => { + const customRole = await config.createRole() + + const res = await config.api.roles.fetch({ + status: 200, + }) + + expect(res.length).toBe(5) + + const adminRole = res.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN) + expect(adminRole).toBeDefined() + expect(adminRole!.inherits).toEqual(BUILTIN_ROLE_IDS.POWER) + expect(adminRole!.permissionId).toEqual(BuiltinPermissionID.ADMIN) + + const powerUserRole = res.find(r => r._id === BUILTIN_ROLE_IDS.POWER) + expect(powerUserRole).toBeDefined() + expect(powerUserRole!.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC) + expect(powerUserRole!.permissionId).toEqual(BuiltinPermissionID.POWER) + + const customRoleFetched = res.find(r => r._id === customRole.name) + expect(customRoleFetched).toBeDefined() + expect(customRoleFetched!.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC) + expect(customRoleFetched!.permissionId).toEqual( + BuiltinPermissionID.READ_ONLY + ) + }) + + it("should be able to get the role with a permission added", async () => { + const table = await config.createTable() + await config.api.permission.add({ + roleId: BUILTIN_ROLE_IDS.POWER, + resourceId: table._id!, + level: PermissionLevel.READ, + }) + const res = await config.api.roles.fetch() + expect(res.length).toBeGreaterThan(0) + const power = res.find(role => role._id === BUILTIN_ROLE_IDS.POWER) + expect(power?.permissions[table._id!]).toEqual(["read"]) + }) + }) + + describe("destroy", () => { + it("should delete custom roles", async () => { + const customRole = await config.createRole({ + name: "user", + permissionId: BuiltinPermissionID.READ_ONLY, + inherits: BUILTIN_ROLE_IDS.BASIC, + }) + await config.api.roles.destroy(customRole, { + status: 200, + }) + await config.api.roles.find(customRole._id!, { + status: 404, + }) + expect(events.role.deleted).toHaveBeenCalledTimes(1) + expect(events.role.deleted).toHaveBeenCalledWith(customRole) + }) + }) + + describe("accessible", () => { + it("should be able to fetch accessible roles (with builder)", async () => { + const res = await config.api.roles.accessible(config.defaultHeaders(), { + status: 200, + }) + expect(res.length).toBe(5) + expect(typeof res[0]).toBe("string") + }) + + it("should be able to fetch accessible roles (basic user)", async () => { + const headers = await config.basicRoleHeaders() + const res = await config.api.roles.accessible(headers, { + status: 200, + }) + expect(res.length).toBe(2) + expect(res[0]).toBe("BASIC") + expect(res[1]).toBe("PUBLIC") + }) + + it("should be able to fetch accessible roles (no user)", async () => { + const res = await config.api.roles.accessible(config.publicHeaders(), { + status: 200, + }) + expect(res.length).toBe(1) + expect(res[0]).toBe("PUBLIC") + }) + + it("should not fetch higher level accessible roles when a custom role header is provided", async () => { + const customRoleName = "CUSTOM_ROLE" + await config.api.roles.save({ + name: customRoleName, + inherits: roles.BUILTIN_ROLE_IDS.BASIC, + permissionId: permissions.BuiltinPermissionID.READ_ONLY, + version: "name", + }) + const res = await config.api.roles.accessible( + { "x-budibase-role": customRoleName }, + { + status: 200, + } + ) + expect(res.length).toBe(3) + expect(res[0]).toBe(customRoleName) + expect(res[1]).toBe("BASIC") + expect(res[2]).toBe("PUBLIC") + }) + }) +}) diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 3d53149385..0255268097 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -517,6 +517,7 @@ export default class TestConfiguration { const headers: any = { Accept: "application/json", + Cookie: "", } if (appId) { headers[constants.Header.APP_ID] = appId diff --git a/packages/server/src/tests/utilities/api/role.ts b/packages/server/src/tests/utilities/api/role.ts index 4defbc1220..31bffc6f85 100644 --- a/packages/server/src/tests/utilities/api/role.ts +++ b/packages/server/src/tests/utilities/api/role.ts @@ -4,6 +4,7 @@ import { FindRoleResponse, SaveRoleRequest, SaveRoleResponse, + Role, } from "@budibase/types" import { Expectations, TestAPI } from "./base" @@ -27,14 +28,18 @@ export class RoleAPI extends TestAPI { }) } - destroy = async (roleId: string, expectations?: Expectations) => { - return await this._delete(`/api/roles/${roleId}`, { + destroy = async (role: Role, expectations?: Expectations) => { + return await this._delete(`/api/roles/${role._id}/${role._rev}`, { expectations, }) } - accesssible = async (expectations?: Expectations) => { + accessible = async ( + headers: Record, + expectations?: Expectations + ) => { return await this._get(`/api/roles/accessible`, { + headers, expectations, }) } diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 698f6d8236..e572447ab4 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -30,6 +30,7 @@ import { BBReferenceFieldSubType, JsonFieldSubType, AutoFieldSubType, + Role, } from "@budibase/types" import { LoopInput } from "../../definitions/automations" import { merge } from "lodash" @@ -492,11 +493,12 @@ export function basicLinkedRow( } } -export function basicRole() { +export function basicRole(): Role { return { name: `NewRole_${utils.newid()}`, inherits: roles.BUILTIN_ROLE_IDS.BASIC, permissionId: permissions.BuiltinPermissionID.READ_ONLY, + permissions: {}, version: "name", } } diff --git a/packages/types/src/api/web/role.ts b/packages/types/src/api/web/role.ts index c37dee60e0..642f815cc4 100644 --- a/packages/types/src/api/web/role.ts +++ b/packages/types/src/api/web/role.ts @@ -4,9 +4,9 @@ export interface SaveRoleRequest { _id?: string _rev?: string name: string - inherits: string + inherits?: string | string[] permissionId: string - version: string + version?: string } export interface SaveRoleResponse extends Role {} diff --git a/packages/types/src/documents/app/role.ts b/packages/types/src/documents/app/role.ts index f32ba810b0..669e8f523c 100644 --- a/packages/types/src/documents/app/role.ts +++ b/packages/types/src/documents/app/role.ts @@ -2,7 +2,7 @@ import { Document } from "../document" export interface Role extends Document { permissionId: string - inherits?: string + inherits?: string | string[] permissions: { [key: string]: string[] } version?: string name: string diff --git a/packages/types/src/sdk/events/role.ts b/packages/types/src/sdk/events/role.ts index b04b9b8ee5..ce17b34dc4 100644 --- a/packages/types/src/sdk/events/role.ts +++ b/packages/types/src/sdk/events/role.ts @@ -3,19 +3,19 @@ import { BaseEvent } from "./event" export interface RoleCreatedEvent extends BaseEvent { roleId: string permissionId: string - inherits?: string + inherits?: string | string[] } export interface RoleUpdatedEvent extends BaseEvent { roleId: string permissionId: string - inherits?: string + inherits?: string | string[] } export interface RoleDeletedEvent extends BaseEvent { roleId: string permissionId: string - inherits?: string + inherits?: string | string[] } export interface RoleAssignedEvent extends BaseEvent { From 10e187014a30a6b5b3703a90735f1984f0d5f632 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 26 Sep 2024 18:02:32 +0100 Subject: [PATCH 2/4] Fixing type issues. --- packages/types/src/api/web/role.ts | 2 +- packages/types/src/documents/app/role.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/types/src/api/web/role.ts b/packages/types/src/api/web/role.ts index 9a768e3ac3..4e56f6cd14 100644 --- a/packages/types/src/api/web/role.ts +++ b/packages/types/src/api/web/role.ts @@ -6,7 +6,7 @@ export interface SaveRoleRequest { name: string inherits?: string | string[] permissionId: string - version: string + version?: string uiMetadata?: RoleUIMetadata } diff --git a/packages/types/src/documents/app/role.ts b/packages/types/src/documents/app/role.ts index 6557b7e19d..22f4ab9cd3 100644 --- a/packages/types/src/documents/app/role.ts +++ b/packages/types/src/documents/app/role.ts @@ -9,7 +9,7 @@ export interface RoleUIMetadata { export interface Role extends Document { permissionId: string - inherits?: string + inherits?: string | string[] permissions: Record version?: string name: string From fa9bb030c923dd62561388e2e3fb4000d8c952d3 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 26 Sep 2024 18:26:35 +0100 Subject: [PATCH 3/4] Utility for detecting loops in a list of roles. --- packages/shared-core/src/helpers/roles.ts | 47 ++++++++++++++ .../src/helpers/tests/roles.spec.ts | 61 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 packages/shared-core/src/helpers/roles.ts create mode 100644 packages/shared-core/src/helpers/tests/roles.spec.ts 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) + }) + }) +}) From d6d4da221da18029f635ab5baf6d9728e9fa9fba Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 27 Sep 2024 17:05:03 +0100 Subject: [PATCH 4/4] Updating role validator. --- packages/server/src/api/routes/utils/validators.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index b589d44b31..925593a8cd 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -226,7 +226,10 @@ export function roleValidator() { ) ) .optional(), - inherits: OPTIONAL_STRING, + inherits: Joi.alternatives().try( + OPTIONAL_STRING, + Joi.array().items(OPTIONAL_STRING) + ), }).unknown(true) ) }