Merge branch 'v3-ui' of github.com:Budibase/budibase into new-rbac-ui
This commit is contained in:
commit
761c9d3c18
|
@ -17,6 +17,7 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import cloneDeep from "lodash/fp/cloneDeep"
|
import cloneDeep from "lodash/fp/cloneDeep"
|
||||||
import { RoleColor, helpers } from "@budibase/shared-core"
|
import { RoleColor, helpers } from "@budibase/shared-core"
|
||||||
|
import { uniqBy } from "lodash"
|
||||||
|
|
||||||
export const BUILTIN_ROLE_IDS = {
|
export const BUILTIN_ROLE_IDS = {
|
||||||
ADMIN: "ADMIN",
|
ADMIN: "ADMIN",
|
||||||
|
@ -37,6 +38,14 @@ export const RoleIDVersion = {
|
||||||
NAME: "name",
|
NAME: "name",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rolesInList(roleIds: string[], ids: string | string[]) {
|
||||||
|
if (Array.isArray(ids)) {
|
||||||
|
return ids.filter(id => roleIds.includes(id)).length === ids.length
|
||||||
|
} else {
|
||||||
|
return roleIds.includes(ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class Role implements RoleDoc {
|
export class Role implements RoleDoc {
|
||||||
_id: string
|
_id: string
|
||||||
_rev?: string
|
_rev?: string
|
||||||
|
@ -66,15 +75,65 @@ export class Role implements RoleDoc {
|
||||||
if (inherits && typeof inherits === "string") {
|
if (inherits && typeof inherits === "string") {
|
||||||
inherits = prefixRoleIDNoBuiltin(inherits)
|
inherits = prefixRoleIDNoBuiltin(inherits)
|
||||||
} else if (inherits && Array.isArray(inherits)) {
|
} else if (inherits && Array.isArray(inherits)) {
|
||||||
inherits = inherits.map(inherit => prefixRoleIDNoBuiltin(inherit))
|
inherits = inherits.map(prefixRoleIDNoBuiltin)
|
||||||
}
|
}
|
||||||
if (inherits) {
|
|
||||||
this.inherits = inherits
|
this.inherits = inherits
|
||||||
}
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RoleHierarchyTraversal {
|
||||||
|
allRoles: RoleDoc[]
|
||||||
|
opts?: { defaultPublic?: boolean }
|
||||||
|
|
||||||
|
constructor(allRoles: RoleDoc[], opts?: { defaultPublic?: boolean }) {
|
||||||
|
this.allRoles = allRoles
|
||||||
|
this.opts = opts
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(role: RoleDoc): RoleDoc[] {
|
||||||
|
const opts = this.opts,
|
||||||
|
allRoles = this.allRoles
|
||||||
|
// this will be a full walked list of roles - which may contain duplicates
|
||||||
|
let roleList: RoleDoc[] = []
|
||||||
|
if (!role || !role._id) {
|
||||||
|
return roleList
|
||||||
|
}
|
||||||
|
roleList.push(role)
|
||||||
|
if (Array.isArray(role.inherits)) {
|
||||||
|
for (let roleId of role.inherits) {
|
||||||
|
const foundRole = findRole(roleId, allRoles, opts)
|
||||||
|
if (foundRole) {
|
||||||
|
roleList = roleList.concat(this.walk(foundRole))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const foundRoleIds: string[] = []
|
||||||
|
let currentRole: RoleDoc | undefined = role
|
||||||
|
while (
|
||||||
|
currentRole &&
|
||||||
|
currentRole.inherits &&
|
||||||
|
!rolesInList(foundRoleIds, currentRole.inherits)
|
||||||
|
) {
|
||||||
|
if (Array.isArray(currentRole.inherits)) {
|
||||||
|
return roleList.concat(this.walk(currentRole))
|
||||||
|
} else {
|
||||||
|
foundRoleIds.push(currentRole.inherits)
|
||||||
|
currentRole = findRole(currentRole.inherits, allRoles, opts)
|
||||||
|
if (currentRole) {
|
||||||
|
roleList.push(currentRole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// loop now found - stop iterating
|
||||||
|
if (helpers.roles.checkForRoleInheritanceLoops(roleList)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqBy(roleList, role => role._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const BUILTIN_ROLES = {
|
const BUILTIN_ROLES = {
|
||||||
ADMIN: new Role(
|
ADMIN: new Role(
|
||||||
BUILTIN_IDS.ADMIN,
|
BUILTIN_IDS.ADMIN,
|
||||||
|
@ -202,7 +261,7 @@ export async function roleToNumber(id: string) {
|
||||||
return findNumber(foundRole) + 1
|
return findNumber(foundRole) + 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(number => !!number)
|
.filter(number => number)
|
||||||
.sort()
|
.sort()
|
||||||
.pop()
|
.pop()
|
||||||
if (highestBuiltin != undefined) {
|
if (highestBuiltin != undefined) {
|
||||||
|
@ -213,12 +272,7 @@ export async function roleToNumber(id: string) {
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
let highest = 0
|
return Math.max(...hierarchy.map(findNumber))
|
||||||
for (let role of hierarchy) {
|
|
||||||
const roleNumber = findNumber(role)
|
|
||||||
highest = Math.max(roleNumber, highest)
|
|
||||||
}
|
|
||||||
return highest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -236,11 +290,23 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
|
||||||
: roleId1
|
: roleId1
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareRoleIds(roleId1: string, roleId2: string) {
|
export function compareRoleIds(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function externalRole(role: RoleDoc): RoleDoc {
|
||||||
|
let _id: string | undefined
|
||||||
|
if (role._id) {
|
||||||
|
_id = getExternalRoleID(role._id)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...role,
|
||||||
|
_id,
|
||||||
|
inherits: getExternalRoleIDs(role.inherits, role.version),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of roles, this will pick the role out, accounting for built ins.
|
* Given a list of roles, this will pick the role out, accounting for built ins.
|
||||||
*/
|
*/
|
||||||
|
@ -293,6 +359,18 @@ export async function getRole(
|
||||||
return findRole(roleId, roleList, opts)
|
return findRole(roleId, roleList, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveRoles(roles: RoleDoc[]) {
|
||||||
|
const db = getAppDB()
|
||||||
|
await db.bulkDocs(
|
||||||
|
roles
|
||||||
|
.filter(role => role._id)
|
||||||
|
.map(role => ({
|
||||||
|
...role,
|
||||||
|
_id: prefixRoleID(role._id!),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple function to get all the roles based on the top level user role ID.
|
* Simple function to get all the roles based on the top level user role ID.
|
||||||
*/
|
*/
|
||||||
|
@ -301,66 +379,19 @@ async function getAllUserRoles(
|
||||||
opts?: { defaultPublic?: boolean }
|
opts?: { defaultPublic?: boolean }
|
||||||
): Promise<RoleDoc[]> {
|
): Promise<RoleDoc[]> {
|
||||||
const allRoles = await getAllRoles()
|
const allRoles = await getAllRoles()
|
||||||
if (helpers.roles.checkForRoleInheritanceLoops(allRoles)) {
|
|
||||||
throw new Error("Loop detected in roles - cannot list roles")
|
|
||||||
}
|
|
||||||
// admins have access to all roles
|
// admins have access to all roles
|
||||||
if (userRoleId === BUILTIN_IDS.ADMIN) {
|
if (userRoleId === BUILTIN_IDS.ADMIN) {
|
||||||
return allRoles
|
return allRoles
|
||||||
}
|
}
|
||||||
const rolesFound = (ids: string | string[]) => {
|
|
||||||
if (Array.isArray(ids)) {
|
|
||||||
return ids.filter(id => roleIds.includes(id)).length === ids.length
|
|
||||||
} else {
|
|
||||||
return roleIds.includes(ids)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleIds = [userRoleId]
|
|
||||||
const roles: RoleDoc[] = []
|
|
||||||
const iterateInherited = (role: RoleDoc | undefined) => {
|
|
||||||
if (!role || !role._id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
roleIds.push(role._id)
|
|
||||||
roles.push(role)
|
|
||||||
if (Array.isArray(role.inherits)) {
|
|
||||||
role.inherits.forEach(roleId => {
|
|
||||||
const foundRole = findRole(roleId, allRoles, opts)
|
|
||||||
if (foundRole) {
|
|
||||||
iterateInherited(foundRole)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
while (role && role.inherits && !rolesFound(role.inherits)) {
|
|
||||||
if (Array.isArray(role.inherits)) {
|
|
||||||
iterateInherited(role)
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
roleIds.push(role.inherits)
|
|
||||||
role = findRole(role.inherits, allRoles, opts)
|
|
||||||
if (role) {
|
|
||||||
roles.push(role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all the inherited roles
|
// get all the inherited roles
|
||||||
const foundRole = findRole(userRoleId, allRoles, opts)
|
const foundRole = findRole(userRoleId, allRoles, opts)
|
||||||
|
let roles: RoleDoc[] = []
|
||||||
if (foundRole) {
|
if (foundRole) {
|
||||||
iterateInherited(foundRole)
|
const traversal = new RoleHierarchyTraversal(allRoles, opts)
|
||||||
|
roles = traversal.walk(foundRole)
|
||||||
}
|
}
|
||||||
const foundRoleIds: string[] = []
|
return roles
|
||||||
return roles.filter(role => {
|
|
||||||
if (role._id && !foundRoleIds.includes(role._id)) {
|
|
||||||
foundRoleIds.push(role._id)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserRoleIdHierarchy(
|
export async function getUserRoleIdHierarchy(
|
||||||
|
|
|
@ -76,9 +76,7 @@
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
open: "error",
|
open: "error",
|
||||||
})
|
})
|
||||||
$goto(
|
$goto(`/builder/app/${appId}/settings/automations?${params.toString()}`)
|
||||||
`/builder/app/${appId}/settings/automation-history?${params.toString()}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorCount = errors => {
|
const errorCount = errors => {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
<script context="module">
|
||||||
|
const NumberFormatter = Intl.NumberFormat()
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TextCell from "./TextCell.svelte"
|
import TextCell from "./TextCell.svelte"
|
||||||
|
|
||||||
|
@ -9,6 +13,24 @@
|
||||||
const newValue = isNaN(float) ? null : float
|
const newValue = isNaN(float) ? null : float
|
||||||
onChange(newValue)
|
onChange(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatNumber = value => {
|
||||||
|
const type = typeof value
|
||||||
|
if (type !== "string" && type !== "number") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (type === "string" && !value.trim().length) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const res = NumberFormatter.format(value)
|
||||||
|
return res === "NaN" ? value : res
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TextCell {...$$props} onChange={numberOnChange} bind:api type="number" />
|
<TextCell
|
||||||
|
{...$$props}
|
||||||
|
onChange={numberOnChange}
|
||||||
|
bind:api
|
||||||
|
type="number"
|
||||||
|
format={formatNumber}
|
||||||
|
/>
|
||||||
|
|
|
@ -7,11 +7,13 @@
|
||||||
export let type = "text"
|
export let type = "text"
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
|
export let format = null
|
||||||
|
|
||||||
let input
|
let input
|
||||||
let active = false
|
let active = false
|
||||||
|
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
|
$: displayValue = format?.(value) ?? value ?? ""
|
||||||
|
|
||||||
const handleChange = e => {
|
const handleChange = e => {
|
||||||
onChange(e.target.value)
|
onChange(e.target.value)
|
||||||
|
@ -52,7 +54,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-cell" class:number={type === "number"}>
|
<div class="text-cell" class:number={type === "number"}>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{value ?? ""}
|
{displayValue}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -28,15 +28,27 @@ const UpdateRolesOptions = {
|
||||||
REMOVED: "removed",
|
REMOVED: "removed",
|
||||||
}
|
}
|
||||||
|
|
||||||
function externalRole(role: Role): Role {
|
async function removeRoleFromOthers(roleId: string) {
|
||||||
let _id: string | undefined
|
const allOtherRoles = await roles.getAllRoles()
|
||||||
if (role._id) {
|
const updated: Role[] = []
|
||||||
_id = roles.getExternalRoleID(role._id)
|
for (let role of allOtherRoles) {
|
||||||
|
let changed = false
|
||||||
|
if (Array.isArray(role.inherits)) {
|
||||||
|
const newInherits = role.inherits.filter(
|
||||||
|
id => !roles.compareRoleIds(id, roleId)
|
||||||
|
)
|
||||||
|
changed = role.inherits.length !== newInherits.length
|
||||||
|
role.inherits = newInherits
|
||||||
|
} else if (role.inherits && roles.compareRoleIds(role.inherits, roleId)) {
|
||||||
|
role.inherits = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
return {
|
if (changed) {
|
||||||
...role,
|
updated.push(role)
|
||||||
_id,
|
}
|
||||||
inherits: roles.getExternalRoleIDs(role.inherits, role.version),
|
}
|
||||||
|
if (updated.length) {
|
||||||
|
await roles.saveRoles(updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,23 +78,25 @@ async function updateRolesOnUserTable(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx<void, FetchRolesResponse>) {
|
export async function fetch(ctx: UserCtx<void, FetchRolesResponse>) {
|
||||||
ctx.body = (await roles.getAllRoles()).map(role => externalRole(role))
|
ctx.body = (await roles.getAllRoles()).map(role => roles.externalRole(role))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: UserCtx<void, FindRoleResponse>) {
|
export async function find(ctx: UserCtx<void, FindRoleResponse>) {
|
||||||
const role = await roles.getRole(ctx.params.roleId)
|
const role = await roles.getRole(ctx.params.roleId)
|
||||||
if (!role) {
|
if (!role) {
|
||||||
ctx.throw(404, { message: "Role not found" })
|
ctx.throw(404, { message: "Role not found" })
|
||||||
} else {
|
|
||||||
ctx.body = externalRole(role)
|
|
||||||
}
|
}
|
||||||
|
ctx.body = roles.externalRole(role)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
|
export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let { _id, name, inherits, permissionId, version, uiMetadata } =
|
let { _id, _rev, name, inherits, permissionId, version, uiMetadata } =
|
||||||
ctx.request.body
|
ctx.request.body
|
||||||
let isCreate = false
|
let isCreate = false
|
||||||
|
if (!_rev && !version) {
|
||||||
|
version = roles.RoleIDVersion.NAME
|
||||||
|
}
|
||||||
const isNewVersion = version === roles.RoleIDVersion.NAME
|
const isNewVersion = version === roles.RoleIDVersion.NAME
|
||||||
|
|
||||||
if (_id && roles.isBuiltin(_id)) {
|
if (_id && roles.isBuiltin(_id)) {
|
||||||
|
@ -131,7 +145,7 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
|
||||||
ctx.throw(400, "Role inheritance contains a loop, this is not supported")
|
ctx.throw(400, "Role inheritance contains a loop, this is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundRev = ctx.request.body._rev || dbRole?._rev
|
const foundRev = _rev || dbRole?._rev
|
||||||
if (foundRev) {
|
if (foundRev) {
|
||||||
role._rev = foundRev
|
role._rev = foundRev
|
||||||
}
|
}
|
||||||
|
@ -148,7 +162,7 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
|
||||||
role.version
|
role.version
|
||||||
)
|
)
|
||||||
role._rev = result.rev
|
role._rev = result.rev
|
||||||
ctx.body = externalRole(role)
|
ctx.body = roles.externalRole(role)
|
||||||
|
|
||||||
const devDb = context.getDevAppDB()
|
const devDb = context.getDevAppDB()
|
||||||
const prodDb = context.getProdAppDB()
|
const prodDb = context.getProdAppDB()
|
||||||
|
@ -198,6 +212,10 @@ export async function destroy(ctx: UserCtx<void, DestroyRoleResponse>) {
|
||||||
UpdateRolesOptions.REMOVED,
|
UpdateRolesOptions.REMOVED,
|
||||||
role.version
|
role.version
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// clean up inherits
|
||||||
|
await removeRoleFromOthers(roleId)
|
||||||
|
|
||||||
ctx.message = `Role ${ctx.params.roleId} deleted successfully`
|
ctx.message = `Role ${ctx.params.roleId} deleted successfully`
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
builderSocket?.emitRoleDeletion(ctx, role)
|
builderSocket?.emitRoleDeletion(ctx, role)
|
||||||
|
|
|
@ -291,6 +291,8 @@ describe("/permission", () => {
|
||||||
describe("multi-inheritance permissions", () => {
|
describe("multi-inheritance permissions", () => {
|
||||||
let table1: Table, table2: Table, role1: Role, role2: Role
|
let table1: Table, table2: Table, role1: Role, role2: Role
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
// create new app
|
||||||
|
await config.init()
|
||||||
table1 = await config.createTable()
|
table1 = await config.createTable()
|
||||||
table2 = await config.createTable()
|
table2 = await config.createTable()
|
||||||
await config.api.row.save(table1._id!, {
|
await config.api.row.save(table1._id!, {
|
||||||
|
@ -301,7 +303,7 @@ describe("/permission", () => {
|
||||||
})
|
})
|
||||||
role1 = await config.api.roles.save(
|
role1 = await config.api.roles.save(
|
||||||
{
|
{
|
||||||
name: "role1",
|
name: "test_1",
|
||||||
permissionId: PermissionLevel.WRITE,
|
permissionId: PermissionLevel.WRITE,
|
||||||
inherits: BUILTIN_ROLE_IDS.BASIC,
|
inherits: BUILTIN_ROLE_IDS.BASIC,
|
||||||
},
|
},
|
||||||
|
@ -309,7 +311,7 @@ describe("/permission", () => {
|
||||||
)
|
)
|
||||||
role2 = await config.api.roles.save(
|
role2 = await config.api.roles.save(
|
||||||
{
|
{
|
||||||
name: "role2",
|
name: "test_2",
|
||||||
permissionId: PermissionLevel.WRITE,
|
permissionId: PermissionLevel.WRITE,
|
||||||
inherits: BUILTIN_ROLE_IDS.BASIC,
|
inherits: BUILTIN_ROLE_IDS.BASIC,
|
||||||
},
|
},
|
||||||
|
@ -328,7 +330,7 @@ describe("/permission", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be unable to search for table 2 using role 1", async () => {
|
it("should be unable to search for table 2 using role 1", async () => {
|
||||||
await config.setRole(role1._id!, async () => {
|
await config.loginAsRole(role1._id!, async () => {
|
||||||
const response2 = await config.api.row.search(
|
const response2 = await config.api.row.search(
|
||||||
table2._id!,
|
table2._id!,
|
||||||
{
|
{
|
||||||
|
@ -347,7 +349,7 @@ describe("/permission", () => {
|
||||||
inherits: [role1._id!, role2._id!],
|
inherits: [role1._id!, role2._id!],
|
||||||
})
|
})
|
||||||
|
|
||||||
await config.setRole(role3._id!, async () => {
|
await config.loginAsRole(role3._id!, async () => {
|
||||||
const response1 = await config.api.row.search(
|
const response1 = await config.api.row.search(
|
||||||
table1._id!,
|
table1._id!,
|
||||||
{
|
{
|
||||||
|
|
|
@ -121,6 +121,34 @@ describe("/roles", () => {
|
||||||
{ status: 400, body: { message: LOOP_ERROR } }
|
{ status: 400, body: { message: LOOP_ERROR } }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("frontend example - should deny", async () => {
|
||||||
|
const id1 = "cb27c4ec9415042f4800411adb346fb7c",
|
||||||
|
id2 = "cbc72a9d61ab64d49b31d90d1df4c1fdb"
|
||||||
|
const role1 = await config.api.roles.save({
|
||||||
|
_id: id1,
|
||||||
|
name: id1,
|
||||||
|
permissions: {},
|
||||||
|
permissionId: "write",
|
||||||
|
version: "name",
|
||||||
|
inherits: ["POWER"],
|
||||||
|
})
|
||||||
|
await config.api.roles.save({
|
||||||
|
_id: id2,
|
||||||
|
permissions: {},
|
||||||
|
name: id2,
|
||||||
|
permissionId: "write",
|
||||||
|
version: "name",
|
||||||
|
inherits: [id1],
|
||||||
|
})
|
||||||
|
await config.api.roles.save(
|
||||||
|
{
|
||||||
|
...role1,
|
||||||
|
inherits: [BUILTIN_ROLE_IDS.POWER, id2],
|
||||||
|
},
|
||||||
|
{ status: 400, body: { message: LOOP_ERROR } }
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
|
@ -189,6 +217,27 @@ describe("/roles", () => {
|
||||||
_id: dbCore.prefixRoleID(customRole._id!),
|
_id: dbCore.prefixRoleID(customRole._id!),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should disconnection roles when deleted", async () => {
|
||||||
|
const role1 = await config.api.roles.save({
|
||||||
|
name: "role1",
|
||||||
|
permissionId: BuiltinPermissionID.WRITE,
|
||||||
|
inherits: [BUILTIN_ROLE_IDS.BASIC],
|
||||||
|
})
|
||||||
|
const role2 = await config.api.roles.save({
|
||||||
|
name: "role2",
|
||||||
|
permissionId: BuiltinPermissionID.WRITE,
|
||||||
|
inherits: [BUILTIN_ROLE_IDS.BASIC, role1._id!],
|
||||||
|
})
|
||||||
|
const role3 = await config.api.roles.save({
|
||||||
|
name: "role3",
|
||||||
|
permissionId: BuiltinPermissionID.WRITE,
|
||||||
|
inherits: [BUILTIN_ROLE_IDS.BASIC, role2._id!],
|
||||||
|
})
|
||||||
|
await config.api.roles.destroy(role2, { status: 200 })
|
||||||
|
const found = await config.api.roles.find(role3._id!, { status: 200 })
|
||||||
|
expect(found.inherits).toEqual([BUILTIN_ROLE_IDS.BASIC])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("accessible", () => {
|
describe("accessible", () => {
|
||||||
|
@ -200,30 +249,36 @@ describe("/roles", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to fetch accessible roles (with builder)", async () => {
|
it("should be able to fetch accessible roles (with builder)", async () => {
|
||||||
const res = await config.api.roles.accessible(config.defaultHeaders(), {
|
await config.withHeaders(config.defaultHeaders(), async () => {
|
||||||
|
const res = await config.api.roles.accessible({
|
||||||
status: 200,
|
status: 200,
|
||||||
})
|
})
|
||||||
expect(res.length).toBe(5)
|
expect(res.length).toBe(5)
|
||||||
expect(typeof res[0]).toBe("string")
|
expect(typeof res[0]).toBe("string")
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to fetch accessible roles (basic user)", async () => {
|
it("should be able to fetch accessible roles (basic user)", async () => {
|
||||||
const headers = await config.basicRoleHeaders()
|
const headers = await config.basicRoleHeaders()
|
||||||
const res = await config.api.roles.accessible(headers, {
|
await config.withHeaders(headers, async () => {
|
||||||
|
const res = await config.api.roles.accessible({
|
||||||
status: 200,
|
status: 200,
|
||||||
})
|
})
|
||||||
expect(res.length).toBe(2)
|
expect(res.length).toBe(2)
|
||||||
expect(res[0]).toBe("BASIC")
|
expect(res[0]).toBe("BASIC")
|
||||||
expect(res[1]).toBe("PUBLIC")
|
expect(res[1]).toBe("PUBLIC")
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to fetch accessible roles (no user)", async () => {
|
it("should be able to fetch accessible roles (no user)", async () => {
|
||||||
const res = await config.api.roles.accessible(config.publicHeaders(), {
|
await config.withHeaders(config.publicHeaders(), async () => {
|
||||||
|
const res = await config.api.roles.accessible({
|
||||||
status: 200,
|
status: 200,
|
||||||
})
|
})
|
||||||
expect(res.length).toBe(1)
|
expect(res.length).toBe(1)
|
||||||
expect(res[0]).toBe("PUBLIC")
|
expect(res[0]).toBe("PUBLIC")
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should not fetch higher level accessible roles when a custom role header is provided", async () => {
|
it("should not fetch higher level accessible roles when a custom role header is provided", async () => {
|
||||||
const customRoleName = "custom_role_1"
|
const customRoleName = "custom_role_1"
|
||||||
|
@ -233,13 +288,15 @@ describe("/roles", () => {
|
||||||
permissionId: permissions.BuiltinPermissionID.READ_ONLY,
|
permissionId: permissions.BuiltinPermissionID.READ_ONLY,
|
||||||
version: "name",
|
version: "name",
|
||||||
})
|
})
|
||||||
const res = await config.api.roles.accessible(
|
await config.withHeaders(
|
||||||
{ "x-budibase-role": customRoleName },
|
{ "x-budibase-role": customRoleName },
|
||||||
{
|
async () => {
|
||||||
|
const res = await config.api.roles.accessible({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
})
|
||||||
|
expect(res).toEqual([customRoleName, "BASIC", "PUBLIC"])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
expect(res).toEqual([customRoleName, "BASIC", "PUBLIC"])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -269,10 +326,12 @@ describe("/roles", () => {
|
||||||
const headers = await config.roleHeaders({
|
const headers = await config.roleHeaders({
|
||||||
roleId: role3,
|
roleId: role3,
|
||||||
})
|
})
|
||||||
const res = await config.api.roles.accessible(headers, {
|
await config.withHeaders(headers, async () => {
|
||||||
|
const res = await config.api.roles.accessible({
|
||||||
status: 200,
|
status: 200,
|
||||||
})
|
})
|
||||||
expect(res).toEqual([role3, role1, "BASIC", "PUBLIC", role2, "POWER"])
|
expect(res).toEqual([role3, role1, "BASIC", "PUBLIC", role2, "POWER"])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -79,7 +79,7 @@ describe("/screens", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function checkScreens(roleId: string, screenIds: string[]) {
|
async function checkScreens(roleId: string, screenIds: string[]) {
|
||||||
await config.setRole(roleId, async () => {
|
await config.loginAsRole(roleId, async () => {
|
||||||
const res = await config.api.application.getDefinition(
|
const res = await config.api.application.getDefinition(
|
||||||
config.prodAppId!,
|
config.prodAppId!,
|
||||||
{
|
{
|
||||||
|
@ -110,6 +110,10 @@ describe("/screens", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("save", () => {
|
describe("save", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to create a screen", async () => {
|
it("should be able to create a screen", async () => {
|
||||||
const screen = basicScreen()
|
const screen = basicScreen()
|
||||||
const responseScreen = await config.api.screen.save(screen, {
|
const responseScreen = await config.api.screen.save(screen, {
|
||||||
|
@ -127,6 +131,7 @@ describe("/screens", () => {
|
||||||
screen._id = responseScreen._id
|
screen._id = responseScreen._id
|
||||||
screen._rev = responseScreen._rev
|
screen._rev = responseScreen._rev
|
||||||
screen.name = "edit"
|
screen.name = "edit"
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
responseScreen = await config.api.screen.save(screen, { status: 200 })
|
responseScreen = await config.api.screen.save(screen, { status: 200 })
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,7 @@ export default class TestConfiguration {
|
||||||
tenantId?: string
|
tenantId?: string
|
||||||
api: API
|
api: API
|
||||||
csrfToken?: string
|
csrfToken?: string
|
||||||
|
temporaryHeaders?: Record<string, string | string[]>
|
||||||
|
|
||||||
constructor(openServer = true) {
|
constructor(openServer = true) {
|
||||||
if (openServer) {
|
if (openServer) {
|
||||||
|
@ -429,10 +430,10 @@ export default class TestConfiguration {
|
||||||
// HEADERS
|
// HEADERS
|
||||||
|
|
||||||
// sets the role for the headers, for the period of a callback
|
// sets the role for the headers, for the period of a callback
|
||||||
async setRole(roleId: string, cb: () => Promise<unknown>) {
|
async loginAsRole(roleId: string, cb: () => Promise<unknown>) {
|
||||||
const roleUser = await this.createUser({
|
const roleUser = await this.createUser({
|
||||||
roles: {
|
roles: {
|
||||||
[this.prodAppId!]: roleId,
|
[this.getProdAppId()]: roleId,
|
||||||
},
|
},
|
||||||
builder: { global: false },
|
builder: { global: false },
|
||||||
admin: { global: false },
|
admin: { global: false },
|
||||||
|
@ -443,17 +444,21 @@ export default class TestConfiguration {
|
||||||
builder: false,
|
builder: false,
|
||||||
prodApp: true,
|
prodApp: true,
|
||||||
})
|
})
|
||||||
const temp = this.user
|
await this.withUser(roleUser, async () => {
|
||||||
this.user = roleUser
|
|
||||||
await cb()
|
await cb()
|
||||||
if (temp) {
|
|
||||||
this.user = temp
|
|
||||||
await this.login({
|
|
||||||
userId: temp._id!,
|
|
||||||
builder: true,
|
|
||||||
prodApp: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async withHeaders(
|
||||||
|
headers: Record<string, string | string[]>,
|
||||||
|
cb: () => Promise<unknown>
|
||||||
|
) {
|
||||||
|
this.temporaryHeaders = headers
|
||||||
|
try {
|
||||||
|
await cb()
|
||||||
|
} finally {
|
||||||
|
this.temporaryHeaders = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultHeaders(extras = {}, prodApp = false) {
|
defaultHeaders(extras = {}, prodApp = false) {
|
||||||
|
@ -479,7 +484,10 @@ export default class TestConfiguration {
|
||||||
} else if (this.appId) {
|
} else if (this.appId) {
|
||||||
headers[constants.Header.APP_ID] = this.appId
|
headers[constants.Header.APP_ID] = this.appId
|
||||||
}
|
}
|
||||||
return headers
|
return {
|
||||||
|
...headers,
|
||||||
|
...this.temporaryHeaders,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
publicHeaders({ prodApp = true } = {}) {
|
publicHeaders({ prodApp = true } = {}) {
|
||||||
|
@ -495,7 +503,10 @@ export default class TestConfiguration {
|
||||||
|
|
||||||
headers[constants.Header.TENANT_ID] = this.getTenantId()
|
headers[constants.Header.TENANT_ID] = this.getTenantId()
|
||||||
|
|
||||||
return headers
|
return {
|
||||||
|
...headers,
|
||||||
|
...this.temporaryHeaders,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async basicRoleHeaders() {
|
async basicRoleHeaders() {
|
||||||
|
|
|
@ -22,10 +22,6 @@ export class RoleAPI extends TestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
save = async (body: SaveRoleRequest, expectations?: Expectations) => {
|
save = async (body: SaveRoleRequest, expectations?: Expectations) => {
|
||||||
// the tests should always be creating the "new" version of roles
|
|
||||||
if (body.version === undefined) {
|
|
||||||
body.version = "name"
|
|
||||||
}
|
|
||||||
return await this._post<SaveRoleResponse>(`/api/roles`, {
|
return await this._post<SaveRoleResponse>(`/api/roles`, {
|
||||||
body,
|
body,
|
||||||
expectations,
|
expectations,
|
||||||
|
@ -38,12 +34,8 @@ export class RoleAPI extends TestAPI {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
accessible = async (
|
accessible = async (expectations?: Expectations) => {
|
||||||
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { Role } from "@budibase/types"
|
import { Role, DocumentType, SEPARATOR } from "@budibase/types"
|
||||||
|
|
||||||
|
// need to have a way to prefix, so we can check if the ID has its prefix or not
|
||||||
|
// all new IDs should be the same in the future, but old roles they are never prefixed
|
||||||
|
// while the role IDs always are - best to check both, also we can't access backend-core here
|
||||||
|
function prefixForCheck(id: string) {
|
||||||
|
return `${DocumentType.ROLE}${SEPARATOR}${id}`
|
||||||
|
}
|
||||||
|
|
||||||
// Function to detect loops in roles
|
// Function to detect loops in roles
|
||||||
export function checkForRoleInheritanceLoops(roles: Role[]): boolean {
|
export function checkForRoleInheritanceLoops(roles: Role[]): boolean {
|
||||||
|
@ -11,16 +18,17 @@ export function checkForRoleInheritanceLoops(roles: Role[]): boolean {
|
||||||
const checking = new Set<string>()
|
const checking = new Set<string>()
|
||||||
|
|
||||||
function hasLoop(roleId: string): boolean {
|
function hasLoop(roleId: string): boolean {
|
||||||
if (checking.has(roleId)) {
|
const prefixed = prefixForCheck(roleId)
|
||||||
|
if (checking.has(roleId) || checking.has(prefixed)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (checked.has(roleId)) {
|
if (checked.has(roleId) || checked.has(prefixed)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
checking.add(roleId)
|
checking.add(roleId)
|
||||||
|
|
||||||
const role = roleMap.get(roleId)
|
const role = roleMap.get(prefixed) || roleMap.get(roleId)
|
||||||
if (!role) {
|
if (!role) {
|
||||||
// role not found - ignore
|
// role not found - ignore
|
||||||
checking.delete(roleId)
|
checking.delete(roleId)
|
||||||
|
|
|
@ -57,5 +57,17 @@ describe("role utilities", () => {
|
||||||
]
|
]
|
||||||
check(true)
|
check(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should handle new and old inherits structure", () => {
|
||||||
|
const role1 = role("role_role_1", "role_1")
|
||||||
|
role("role_role_2", ["role_1"])
|
||||||
|
role1.inherits = "role_2"
|
||||||
|
check(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("self reference contains loop", () => {
|
||||||
|
role("role1", "role1")
|
||||||
|
check(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Role, RoleUIMetadata } from "../../documents"
|
import { Role, RoleUIMetadata } from "../../documents"
|
||||||
|
import { PermissionLevel } from "../../sdk"
|
||||||
|
|
||||||
export interface SaveRoleRequest {
|
export interface SaveRoleRequest {
|
||||||
_id?: string
|
_id?: string
|
||||||
|
@ -6,6 +7,7 @@ export interface SaveRoleRequest {
|
||||||
name: string
|
name: string
|
||||||
inherits?: string | string[]
|
inherits?: string | string[]
|
||||||
permissionId: string
|
permissionId: string
|
||||||
|
permissions?: Record<string, PermissionLevel[]>
|
||||||
version?: string
|
version?: string
|
||||||
uiMetadata?: RoleUIMetadata
|
uiMetadata?: RoleUIMetadata
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue