Merge branch 'develop' into design-section-feature-branch

This commit is contained in:
Andrew Kingston 2023-08-24 09:23:30 +01:00 committed by GitHub
commit a6e000ed34
13 changed files with 259 additions and 53 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.9.30-alpha.11", "version": "2.9.30-alpha.12",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -102,6 +102,10 @@ export const useAppBuilders = () => {
return useFeature(Feature.APP_BUILDERS) return useFeature(Feature.APP_BUILDERS)
} }
export const useViewPermissions = () => {
return useFeature(Feature.VIEW_PERMISSIONS)
}
// QUOTAS // QUOTAS
export const setAutomationLogsQuota = (value: number) => { export const setAutomationLogsQuota = (value: number) => {

@ -1 +1 @@
Subproject commit af8a40089809485712c7ef626d9e04090ef09975 Subproject commit b7815e099bbd5e1410185c464dbd54f7287e732f

View File

@ -1,11 +1,12 @@
import { permissions, roles, context } from "@budibase/backend-core" import { permissions, roles, context, HTTPError } from "@budibase/backend-core"
import { UserCtx, Database, Role, PermissionLevel } from "@budibase/types"
import { getRoleParams } from "../../db/utils" import { getRoleParams } from "../../db/utils"
import { import {
CURRENTLY_SUPPORTED_LEVELS, CURRENTLY_SUPPORTED_LEVELS,
getBasePermissions, getBasePermissions,
} from "../../utilities/security" } from "../../utilities/security"
import { removeFromArray } from "../../utilities" import { removeFromArray } from "../../utilities"
import { UserCtx, Database, Role } from "@budibase/types" import sdk from "../../sdk"
const PermissionUpdateType = { const PermissionUpdateType = {
REMOVE: "remove", REMOVE: "remove",
@ -25,14 +26,25 @@ async function getAllDBRoles(db: Database) {
} }
async function updatePermissionOnRole( async function updatePermissionOnRole(
appId: string,
{ {
roleId, roleId,
resourceId, resourceId,
level, level,
}: { roleId: string; resourceId: string; level: string }, }: { roleId: string; resourceId: string; level: PermissionLevel },
updateType: string updateType: string
) { ) {
const allowedAction = await sdk.permissions.resourceActionAllowed({
resourceId,
level,
})
if (!allowedAction.allowed) {
throw new HTTPError(
`You are not allowed to '${allowedAction.level}' the resource type '${allowedAction.resourceType}'`,
403
)
}
const db = context.getAppDB() const db = context.getAppDB()
const remove = updateType === PermissionUpdateType.REMOVE const remove = updateType === PermissionUpdateType.REMOVE
const isABuiltin = roles.isBuiltin(roleId) const isABuiltin = roles.isBuiltin(roleId)
@ -163,16 +175,11 @@ export async function getResourcePerms(ctx: UserCtx) {
} }
export async function addPermission(ctx: UserCtx) { export async function addPermission(ctx: UserCtx) {
ctx.body = await updatePermissionOnRole( ctx.body = await updatePermissionOnRole(ctx.params, PermissionUpdateType.ADD)
ctx.appId,
ctx.params,
PermissionUpdateType.ADD
)
} }
export async function removePermission(ctx: UserCtx) { export async function removePermission(ctx: UserCtx) {
ctx.body = await updatePermissionOnRole( ctx.body = await updatePermissionOnRole(
ctx.appId,
ctx.params, ctx.params,
PermissionUpdateType.REMOVE PermissionUpdateType.REMOVE
) )

View File

@ -1,5 +1,20 @@
const { roles } = require("@budibase/backend-core") const mockedSdk = sdk.permissions as jest.Mocked<typeof sdk.permissions>
const setup = require("./utilities") jest.mock("../../../sdk/app/permissions", () => ({
resourceActionAllowed: jest.fn(),
}))
import sdk from "../../../sdk"
import { roles } from "@budibase/backend-core"
import {
Document,
DocumentType,
PermissionLevel,
Row,
Table,
} from "@budibase/types"
import * as setup from "./utilities"
const { basicRow } = setup.structures const { basicRow } = setup.structures
const { BUILTIN_ROLE_IDS } = roles const { BUILTIN_ROLE_IDS } = roles
@ -9,29 +24,27 @@ const STD_ROLE_ID = BUILTIN_ROLE_IDS.PUBLIC
describe("/permission", () => { describe("/permission", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let table let table: Table & { _id: string }
let perms let perms: Document[]
let row let row: Row
afterAll(setup.afterAll) afterAll(setup.afterAll)
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
}) })
beforeEach(async () => {
table = await config.createTable()
row = await config.createRow()
perms = await config.addPermission(STD_ROLE_ID, table._id)
})
async function getTablePermissions() { beforeEach(async () => {
return request mockedSdk.resourceActionAllowed.mockResolvedValue({ allowed: true })
.get(`/api/permission/${table._id}`)
.set(config.defaultHeaders()) table = (await config.createTable()) as typeof table
.expect("Content-Type", /json/) row = await config.createRow()
.expect(200) perms = await config.api.permission.set({
} roleId: STD_ROLE_ID,
resourceId: table._id,
level: PermissionLevel.READ,
})
})
describe("levels", () => { describe("levels", () => {
it("should be able to get levels", async () => { it("should be able to get levels", async () => {
@ -65,8 +78,12 @@ describe("/permission", () => {
}) })
it("should get resource permissions with multiple roles", async () => { it("should get resource permissions with multiple roles", async () => {
perms = await config.addPermission(HIGHER_ROLE_ID, table._id, "write") perms = await config.api.permission.set({
const res = await getTablePermissions() roleId: HIGHER_ROLE_ID,
resourceId: table._id,
level: PermissionLevel.WRITE,
})
const res = await config.api.permission.get(table._id)
expect(res.body["read"]).toEqual(STD_ROLE_ID) expect(res.body["read"]).toEqual(STD_ROLE_ID)
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID) expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
const allRes = await request const allRes = await request
@ -77,19 +94,59 @@ describe("/permission", () => {
expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID) expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID)
expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID) expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID)
}) })
it("throw forbidden if the action is not allowed for the resource", async () => {
mockedSdk.resourceActionAllowed.mockResolvedValue({
allowed: false,
resourceType: DocumentType.DATASOURCE,
level: PermissionLevel.READ,
})
const response = await config.api.permission.set(
{
roleId: STD_ROLE_ID,
resourceId: table._id,
level: PermissionLevel.EXECUTE,
},
{ expectStatus: 403 }
)
expect(response.message).toEqual(
"You are not allowed to 'read' the resource type 'datasource'"
)
})
}) })
describe("remove", () => { describe("remove", () => {
it("should be able to remove the permission", async () => { it("should be able to remove the permission", async () => {
const res = await request const res = await config.api.permission.revoke({
.delete(`/api/permission/${STD_ROLE_ID}/${table._id}/read`) roleId: STD_ROLE_ID,
.set(config.defaultHeaders()) resourceId: table._id,
.expect("Content-Type", /json/) level: PermissionLevel.READ,
.expect(200) })
expect(res.body[0]._id).toEqual(STD_ROLE_ID) expect(res.body[0]._id).toEqual(STD_ROLE_ID)
const permsRes = await getTablePermissions() const permsRes = await config.api.permission.get(table._id)
expect(permsRes.body[STD_ROLE_ID]).toBeUndefined() expect(permsRes.body[STD_ROLE_ID]).toBeUndefined()
}) })
it("throw forbidden if the action is not allowed for the resource", async () => {
mockedSdk.resourceActionAllowed.mockResolvedValue({
allowed: false,
resourceType: DocumentType.DATASOURCE,
level: PermissionLevel.READ,
})
const response = await config.api.permission.revoke(
{
roleId: STD_ROLE_ID,
resourceId: table._id,
level: PermissionLevel.EXECUTE,
},
{ expectStatus: 403 }
)
expect(response.body.message).toEqual(
"You are not allowed to 'read' the resource type 'datasource'"
)
})
}) })
describe("check public user allowed", () => { describe("check public user allowed", () => {
@ -124,7 +181,9 @@ describe("/permission", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(Array.isArray(res.body)).toEqual(true) expect(Array.isArray(res.body)).toEqual(true)
const publicPerm = res.body.find(perm => perm._id === "public") const publicPerm = res.body.find(
(perm: Document) => perm._id === "public"
)
expect(publicPerm).toBeDefined() expect(publicPerm).toBeDefined()
expect(publicPerm.permissions).toBeDefined() expect(publicPerm.permissions).toBeDefined()
expect(publicPerm.name).toBeDefined() expect(publicPerm.name).toBeDefined()

View File

@ -1,5 +1,6 @@
const { roles, events, permissions } = require("@budibase/backend-core") const { roles, events, permissions } = require("@budibase/backend-core")
const setup = require("./utilities") const setup = require("./utilities")
const { PermissionLevel } = require("@budibase/types")
const { basicRole } = setup.structures const { basicRole } = setup.structures
const { BUILTIN_ROLE_IDS } = roles const { BUILTIN_ROLE_IDS } = roles
const { BuiltinPermissionID } = permissions const { BuiltinPermissionID } = permissions
@ -16,7 +17,7 @@ describe("/roles", () => {
const createRole = async (role) => { const createRole = async (role) => {
if (!role) { if (!role) {
role = basicRole() role = basicRole()
} }
return request return request
@ -98,7 +99,7 @@ describe("/roles", () => {
it("should be able to get the role with a permission added", async () => { it("should be able to get the role with a permission added", async () => {
const table = await config.createTable() const table = await config.createTable()
await config.addPermission(BUILTIN_ROLE_IDS.POWER, table._id) await config.api.permission.set({ roleId: BUILTIN_ROLE_IDS.POWER, resourceId: table._id, level: PermissionLevel.READ })
const res = await request const res = await request
.get(`/api/roles`) .get(`/api/roles`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())

View File

@ -0,0 +1,37 @@
import {
DocumentType,
PermissionLevel,
VirtualDocumentType,
} from "@budibase/types"
import { isViewID } from "../../../db/utils"
import { features } from "@budibase/pro"
type ResourceActionAllowedResult =
| { allowed: true }
| {
allowed: false
level: PermissionLevel
resourceType: DocumentType | VirtualDocumentType
}
export async function resourceActionAllowed({
resourceId,
level,
}: {
resourceId: string
level: PermissionLevel
}): Promise<ResourceActionAllowedResult> {
if (!isViewID(resourceId)) {
return { allowed: true }
}
if (await features.isViewPermissionEnabled()) {
return { allowed: true }
}
return {
allowed: false,
level,
resourceType: VirtualDocumentType.VIEW,
}
}

View File

@ -0,0 +1,52 @@
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
import { PermissionLevel } from "@budibase/types"
import { mocks, structures } from "@budibase/backend-core/tests"
import { resourceActionAllowed } from ".."
import { generateViewID } from "../../../../db/utils"
describe("permissions sdk", () => {
beforeEach(() => {
new TestConfiguration()
mocks.licenses.useCloudFree()
})
describe("resourceActionAllowed", () => {
it("non view resources actions are always allowed", async () => {
const resourceId = structures.users.user()._id!
const result = await resourceActionAllowed({
resourceId,
level: PermissionLevel.READ,
})
expect(result).toEqual({ allowed: true })
})
it("view resources actions allowed if the feature flag is enabled", async () => {
mocks.licenses.useViewPermissions()
const resourceId = generateViewID(structures.generator.guid())
const result = await resourceActionAllowed({
resourceId,
level: PermissionLevel.READ,
})
expect(result).toEqual({ allowed: true })
})
it("view resources actions allowed if the feature flag is disabled", async () => {
const resourceId = generateViewID(structures.generator.guid())
const result = await resourceActionAllowed({
resourceId,
level: PermissionLevel.READ,
})
expect(result).toEqual({
allowed: false,
level: "read",
resourceType: "view",
})
})
})
})

View File

@ -8,6 +8,7 @@ import { default as rows } from "./app/rows"
import { default as users } from "./users" import { default as users } from "./users"
import { default as plugins } from "./plugins" import { default as plugins } from "./plugins"
import * as views from "./app/views" import * as views from "./app/views"
import * as permissions from "./app/permissions"
const sdk = { const sdk = {
backups, backups,
@ -20,6 +21,7 @@ const sdk = {
queries, queries,
plugins, plugins,
views, views,
permissions,
} }
// default export for TS // default export for TS

View File

@ -620,18 +620,6 @@ class TestConfiguration {
return this._req(config, null, controllers.role.save) return this._req(config, null, controllers.role.save)
} }
async addPermission(roleId: string, resourceId: string, level = "read") {
return this._req(
null,
{
roleId,
resourceId,
level,
},
controllers.perms.addPermission
)
}
// VIEW // VIEW
async createView(config?: any) { async createView(config?: any) {

View File

@ -1,4 +1,5 @@
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { PermissionAPI } from "./permission"
import { RowAPI } from "./row" import { RowAPI } from "./row"
import { TableAPI } from "./table" import { TableAPI } from "./table"
import { ViewV2API } from "./viewV2" import { ViewV2API } from "./viewV2"
@ -7,10 +8,12 @@ export default class API {
table: TableAPI table: TableAPI
viewV2: ViewV2API viewV2: ViewV2API
row: RowAPI row: RowAPI
permission: PermissionAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
this.viewV2 = new ViewV2API(config) this.viewV2 = new ViewV2API(config)
this.row = new RowAPI(config) this.row = new RowAPI(config)
this.permission = new PermissionAPI(config)
} }
} }

View File

@ -0,0 +1,52 @@
import { AnyDocument, PermissionLevel } from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class PermissionAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
get = async (
resourceId: string,
{ expectStatus } = { expectStatus: 200 }
) => {
return this.request
.get(`/api/permission/${resourceId}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(expectStatus)
}
set = async (
{
roleId,
resourceId,
level,
}: { roleId: string; resourceId: string; level: PermissionLevel },
{ expectStatus } = { expectStatus: 200 }
): Promise<any> => {
const res = await this.request
.post(`/api/permission/${roleId}/${resourceId}/${level}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(expectStatus)
return res.body
}
revoke = async (
{
roleId,
resourceId,
level,
}: { roleId: string; resourceId: string; level: PermissionLevel },
{ expectStatus } = { expectStatus: 200 }
) => {
const res = await this.request
.delete(`/api/permission/${roleId}/${resourceId}/${level}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(expectStatus)
return res
}
}

View File

@ -12,6 +12,7 @@ export enum Feature {
APP_BUILDERS = "appBuilders", APP_BUILDERS = "appBuilders",
OFFLINE = "offline", OFFLINE = "offline",
USER_ROLE_PUBLIC_API = "userRolePublicApi", USER_ROLE_PUBLIC_API = "userRolePublicApi",
VIEW_PERMISSIONS = "viewPermission",
} }
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined } export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }