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.

This commit is contained in:
mike12345567 2024-08-05 15:45:49 +01:00
parent f07ebc18db
commit 0c8228edad
10 changed files with 218 additions and 203 deletions

View File

@ -42,7 +42,7 @@ export class Role implements RoleDoc {
_rev?: string _rev?: string
name: string name: string
permissionId: string permissionId: string
inherits?: string inherits?: string | string[]
version?: string version?: string
permissions = {} permissions = {}
@ -54,8 +54,10 @@ export class Role implements RoleDoc {
this.version = RoleIDVersion.NAME this.version = RoleIDVersion.NAME
} }
addInheritance(inherits: string) { addInheritance(inherits?: string | string[]) {
if (inherits) {
this.inherits = inherits this.inherits = inherits
}
return this return this
} }
} }
@ -113,7 +115,11 @@ export function builtinRoleToNumber(id: string) {
if (!role) { if (!role) {
break break
} }
if (Array.isArray(role.inherits)) {
// TODO: role inheritance
} else {
role = builtins[role.inherits!] role = builtins[role.inherits!]
}
count++ count++
} while (role !== null) } while (role !== null)
return count return count
@ -130,7 +136,12 @@ export async function roleToNumber(id: string) {
defaultPublic: true, defaultPublic: true,
})) as RoleDoc[] })) as RoleDoc[]
for (let role of hierarchy) { 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 return builtinRoleToNumber(role.inherits) + 1
} }
} }
@ -202,18 +213,29 @@ async function getAllUserRoles(
let currentRole = await getRole(userRoleId, opts) let currentRole = await getRole(userRoleId, opts)
let roles = currentRole ? [currentRole] : [] let roles = currentRole ? [currentRole] : []
let roleIds = [userRoleId] 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 // get all the inherited roles
while ( while (
currentRole && currentRole &&
currentRole.inherits && currentRole.inherits &&
roleIds.indexOf(currentRole.inherits) === -1 !rolesFound(currentRole.inherits)
) { ) {
if (Array.isArray(currentRole.inherits)) {
// TODO: role inheritance
} else {
roleIds.push(currentRole.inherits) roleIds.push(currentRole.inherits)
currentRole = await getRole(currentRole.inherits) currentRole = await getRole(currentRole.inherits)
if (currentRole) { if (currentRole) {
roles.push(currentRole) roles.push(currentRole)
} }
} }
}
return roles return roles
} }

View File

@ -182,7 +182,10 @@ export async function accessible(ctx: UserCtx<void, AccessibleRolesResponse>) {
let filteredRoles = [roleHeader] let filteredRoles = [roleHeader]
for (let role of orderedRoles) { for (let role of orderedRoles) {
filteredRoles = [role, ...filteredRoles] filteredRoles = [role, ...filteredRoles]
if (role === inherits) { if (
(Array.isArray(inherits) && inherits.includes(role)) ||
role === inherits
) {
break break
} }
} }

View File

@ -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")
})
})
})

View File

@ -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")
})
})
})

View File

@ -517,6 +517,7 @@ export default class TestConfiguration {
const headers: any = { const headers: any = {
Accept: "application/json", Accept: "application/json",
Cookie: "",
} }
if (appId) { if (appId) {
headers[constants.Header.APP_ID] = appId headers[constants.Header.APP_ID] = appId

View File

@ -4,6 +4,7 @@ import {
FindRoleResponse, FindRoleResponse,
SaveRoleRequest, SaveRoleRequest,
SaveRoleResponse, SaveRoleResponse,
Role,
} from "@budibase/types" } from "@budibase/types"
import { Expectations, TestAPI } from "./base" import { Expectations, TestAPI } from "./base"
@ -27,14 +28,18 @@ export class RoleAPI extends TestAPI {
}) })
} }
destroy = async (roleId: string, expectations?: Expectations) => { destroy = async (role: Role, expectations?: Expectations) => {
return await this._delete(`/api/roles/${roleId}`, { return await this._delete(`/api/roles/${role._id}/${role._rev}`, {
expectations, expectations,
}) })
} }
accesssible = async (expectations?: Expectations) => { accessible = async (
headers: Record<string, string | string[]>,
expectations?: Expectations
) => {
return await this._get<AccessibleRolesResponse>(`/api/roles/accessible`, { return await this._get<AccessibleRolesResponse>(`/api/roles/accessible`, {
headers,
expectations, expectations,
}) })
} }

View File

@ -30,6 +30,7 @@ import {
BBReferenceFieldSubType, BBReferenceFieldSubType,
JsonFieldSubType, JsonFieldSubType,
AutoFieldSubType, AutoFieldSubType,
Role,
} from "@budibase/types" } from "@budibase/types"
import { LoopInput } from "../../definitions/automations" import { LoopInput } from "../../definitions/automations"
import { merge } from "lodash" import { merge } from "lodash"
@ -492,11 +493,12 @@ export function basicLinkedRow(
} }
} }
export function basicRole() { export function basicRole(): Role {
return { return {
name: `NewRole_${utils.newid()}`, name: `NewRole_${utils.newid()}`,
inherits: roles.BUILTIN_ROLE_IDS.BASIC, inherits: roles.BUILTIN_ROLE_IDS.BASIC,
permissionId: permissions.BuiltinPermissionID.READ_ONLY, permissionId: permissions.BuiltinPermissionID.READ_ONLY,
permissions: {},
version: "name", version: "name",
} }
} }

View File

@ -4,9 +4,9 @@ export interface SaveRoleRequest {
_id?: string _id?: string
_rev?: string _rev?: string
name: string name: string
inherits: string inherits?: string | string[]
permissionId: string permissionId: string
version: string version?: string
} }
export interface SaveRoleResponse extends Role {} export interface SaveRoleResponse extends Role {}

View File

@ -2,7 +2,7 @@ import { Document } from "../document"
export interface Role extends Document { export interface Role extends Document {
permissionId: string permissionId: string
inherits?: string inherits?: string | string[]
permissions: { [key: string]: string[] } permissions: { [key: string]: string[] }
version?: string version?: string
name: string name: string

View File

@ -3,19 +3,19 @@ import { BaseEvent } from "./event"
export interface RoleCreatedEvent extends BaseEvent { export interface RoleCreatedEvent extends BaseEvent {
roleId: string roleId: string
permissionId: string permissionId: string
inherits?: string inherits?: string | string[]
} }
export interface RoleUpdatedEvent extends BaseEvent { export interface RoleUpdatedEvent extends BaseEvent {
roleId: string roleId: string
permissionId: string permissionId: string
inherits?: string inherits?: string | string[]
} }
export interface RoleDeletedEvent extends BaseEvent { export interface RoleDeletedEvent extends BaseEvent {
roleId: string roleId: string
permissionId: string permissionId: string
inherits?: string inherits?: string | string[]
} }
export interface RoleAssignedEvent extends BaseEvent { export interface RoleAssignedEvent extends BaseEvent {