Merge pull request #11655 from Budibase/BUDI-7393/use_permissions_on_middleware

Use permission sdk on middleware
This commit is contained in:
Adria Navarro 2023-09-04 16:33:50 +02:00 committed by GitHub
commit 25b82dd75a
6 changed files with 138 additions and 119 deletions

View File

@ -11,7 +11,11 @@ export function getDB(dbName?: string, opts?: any): Database {
// we have to use a callback for this so that we can close // we have to use a callback for this so that we can close
// the DB when we're done, without this manual requests would // the DB when we're done, without this manual requests would
// need to close the database when done with it to avoid memory leaks // need to close the database when done with it to avoid memory leaks
export async function doWithDB(dbName: string, cb: any, opts = {}) { export async function doWithDB<T>(
dbName: string,
cb: (db: Database) => Promise<T>,
opts = {}
) {
const db = getDB(dbName, opts) const db = getDB(dbName, opts)
// need this to be async so that we can correctly close DB after all // need this to be async so that we can correctly close DB after all
// async operations have been completed // async operations have been completed

View File

@ -253,7 +253,7 @@ export function checkForRoleResourceArray(
* Given an app ID this will retrieve all of the roles that are currently within that app. * Given an app ID this will retrieve all of the roles that are currently within that app.
* @return {Promise<object[]>} An array of the role objects that were found. * @return {Promise<object[]>} An array of the role objects that were found.
*/ */
export async function getAllRoles(appId?: string) { export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
if (appId) { if (appId) {
return doWithDB(appId, internal) return doWithDB(appId, internal)
} else { } else {

View File

@ -1,11 +1,12 @@
import tk from "timekeeper" import tk from "timekeeper"
import { outputProcessing } from "../../../utilities/rowProcessor" import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities" import * as setup from "./utilities"
import { context, tenancy } from "@budibase/backend-core" import { context, roles, tenancy } from "@budibase/backend-core"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { import {
FieldType, FieldType,
MonthlyQuotaName, MonthlyQuotaName,
PermissionLevel,
QuotaUsageType, QuotaUsageType,
Row, Row,
SortOrder, SortOrder,
@ -16,6 +17,7 @@ import {
import { import {
expectAnyInternalColsAttributes, expectAnyInternalColsAttributes,
generator, generator,
mocks,
structures, structures,
} from "@budibase/backend-core/tests" } from "@budibase/backend-core/tests"
@ -37,6 +39,7 @@ describe("/rows", () => {
}) })
beforeEach(async () => { beforeEach(async () => {
mocks.licenses.useCloudFree()
table = await config.createTable() table = await config.createTable()
row = basicRow(table._id!) row = basicRow(table._id!)
}) })
@ -1314,6 +1317,85 @@ describe("/rows", () => {
bookmark: expect.any(String), bookmark: expect.any(String),
}) })
}) })
describe("permissions", () => {
let viewId: string
let tableId: string
beforeAll(async () => {
const table = await config.createTable(userTable())
const rows = []
for (let i = 0; i < 10; i++) {
rows.push(await config.createRow({ tableId: table._id }))
}
const createViewResponse = await config.api.viewV2.create()
tableId = table._id!
viewId = createViewResponse.id
})
beforeEach(() => {
mocks.licenses.useViewPermissions()
})
it("does not allow public users to fetch by default", async () => {
await config.publish()
await config.api.viewV2.search(viewId, undefined, {
expectStatus: 403,
usePublicUser: true,
})
})
it("allow public users to fetch when permissions are explicit", async () => {
await config.api.permission.set({
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC,
level: PermissionLevel.READ,
resourceId: viewId,
})
await config.publish()
const response = await config.api.viewV2.search(viewId, undefined, {
usePublicUser: true,
})
expect(response.body.rows).toHaveLength(10)
})
it("allow public users to fetch when permissions are inherited", async () => {
await config.api.permission.set({
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC,
level: PermissionLevel.READ,
resourceId: tableId,
})
await config.publish()
const response = await config.api.viewV2.search(viewId, undefined, {
usePublicUser: true,
})
expect(response.body.rows).toHaveLength(10)
})
it("respects inherited permissions, not allowing not public views from public tables", async () => {
await config.api.permission.set({
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC,
level: PermissionLevel.READ,
resourceId: tableId,
})
await config.api.permission.set({
roleId: roles.BUILTIN_ROLE_IDS.POWER,
level: PermissionLevel.READ,
resourceId: viewId,
})
await config.publish()
await config.api.viewV2.search(viewId, undefined, {
usePublicUser: true,
expectStatus: 403,
})
})
})
}) })
}) })
}) })

View File

@ -6,11 +6,10 @@ import {
users, users,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types" import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types"
import { features } from "@budibase/pro"
import builderMiddleware from "./builder" import builderMiddleware from "./builder"
import { isWebhookEndpoint } from "./utils" import { isWebhookEndpoint } from "./utils"
import { paramResource } from "./resourceId" import { paramResource } from "./resourceId"
import { extractViewInfoFromID, isViewID } from "../db/utils" import sdk from "../sdk"
function hasResource(ctx: any) { function hasResource(ctx: any) {
return ctx.resourceId != null return ctx.resourceId != null
@ -77,31 +76,6 @@ const checkAuthorizedResource = async (
} }
} }
const resourceIdTranformers: Partial<
Record<PermissionType, (ctx: UserCtx) => Promise<void>>
> = {
[PermissionType.VIEW]: async ctx => {
const { resourceId } = ctx
if (!resourceId) {
ctx.throw(400, `Cannot obtain the view id`)
return
}
if (!isViewID(resourceId)) {
ctx.throw(400, `"${resourceId}" is not a valid view id`)
return
}
if (await features.isViewPermissionEnabled()) {
ctx.subResourceId = ctx.resourceId
ctx.resourceId = extractViewInfoFromID(resourceId).tableId
} else {
ctx.resourceId = extractViewInfoFromID(resourceId).tableId
delete ctx.subResourceId
}
},
}
const authorized = const authorized =
( (
permType: PermissionType, permType: PermissionType,
@ -121,8 +95,8 @@ const authorized =
} }
// get the resource roles // get the resource roles
let resourceRoles: any = [] let resourceRoles: string[] = []
let otherLevelRoles: any = [] let otherLevelRoles: string[] = []
const otherLevel = const otherLevel =
permLevel === PermissionLevel.READ permLevel === PermissionLevel.READ
? PermissionLevel.WRITE ? PermissionLevel.WRITE
@ -133,21 +107,28 @@ const authorized =
paramResource(resourcePath)(ctx, () => {}) paramResource(resourcePath)(ctx, () => {})
} }
if (resourceIdTranformers[permType]) {
await resourceIdTranformers[permType]!(ctx)
}
if (hasResource(ctx)) { if (hasResource(ctx)) {
const { resourceId, subResourceId } = ctx const { resourceId, subResourceId } = ctx
resourceRoles = await roles.getRequiredResourceRole(permLevel!, {
resourceId, const permissions = await sdk.permissions.getResourcePerms(resourceId)
subResourceId, const subPermissions =
}) !!subResourceId &&
(await sdk.permissions.getResourcePerms(subResourceId))
function getPermLevel(permLevel: string) {
let result: string[] = []
if (permissions[permLevel]) {
result.push(permissions[permLevel].role)
}
if (subPermissions && subPermissions[permLevel]) {
result.push(subPermissions[permLevel].role)
}
return result
}
resourceRoles = getPermLevel(permLevel!)
if (opts && opts.schema) { if (opts && opts.schema) {
otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, { otherLevelRoles = getPermLevel(otherLevel!)
resourceId,
subResourceId,
})
} }
} }

View File

@ -1,28 +1,20 @@
jest.mock("@budibase/backend-core", () => ({ jest.mock("../../sdk/app/permissions", () => ({
...jest.requireActual("@budibase/backend-core"), ...jest.requireActual("../../sdk/app/permissions"),
roles: { getResourcePerms: jest.fn().mockResolvedValue([]),
...jest.requireActual("@budibase/backend-core").roles,
getRequiredResourceRole: jest.fn().mockResolvedValue([]),
},
}))
jest.mock("../../environment", () => ({
prod: false,
isTest: () => true,
// @ts-ignore
isProd: () => this.prod,
_set: function (_key: string, value: string) {
this.prod = value === "production"
},
})) }))
import { PermissionType, PermissionLevel } from "@budibase/types" import {
PermissionType,
PermissionLevel,
PermissionSource,
} from "@budibase/types"
import authorizedMiddleware from "../authorized" import authorizedMiddleware from "../authorized"
import env from "../../environment" import env from "../../environment"
import { generateTableID, generateViewID } from "../../db/utils" import { generateTableID, generateViewID } from "../../db/utils"
import { roles } from "@budibase/backend-core" import { generator, mocks } from "@budibase/backend-core/tests"
import { mocks } from "@budibase/backend-core/tests"
import { initProMocks } from "../../tests/utilities/mocks/pro" import { initProMocks } from "../../tests/utilities/mocks/pro"
import { getResourcePerms } from "../../sdk/app/permissions"
const APP_ID = "" const APP_ID = ""
@ -189,13 +181,10 @@ describe("Authorization middleware", () => {
) )
}) })
describe("view type", () => { describe("with resource", () => {
const tableId = generateTableID() let resourceId: string
const viewId = generateViewID(tableId) const mockedGetResourcePerms = getResourcePerms as jest.MockedFunction<
typeof getResourcePerms
const mockedGetRequiredResourceRole =
roles.getRequiredResourceRole as jest.MockedFunction<
typeof roles.getRequiredResourceRole
> >
beforeEach(() => { beforeEach(() => {
@ -203,9 +192,15 @@ describe("Authorization middleware", () => {
PermissionType.VIEW, PermissionType.VIEW,
PermissionLevel.READ PermissionLevel.READ
) )
config.setResourceId(viewId) resourceId = generator.guid()
config.setResourceId(resourceId)
mockedGetRequiredResourceRole.mockResolvedValue(["PUBLIC"]) mockedGetResourcePerms.mockResolvedValue({
[PermissionLevel.READ]: {
role: "PUBLIC",
type: PermissionSource.BASE,
},
})
config.setUser({ config.setUser({
_id: "user", _id: "user",
@ -215,57 +210,14 @@ describe("Authorization middleware", () => {
}) })
}) })
it("will ignore view permissions if flag is off", async () => { it("will fetch resource permissions when resource is set", async () => {
await config.executeMiddleware() await config.executeMiddleware()
expect(config.throw).not.toBeCalled() expect(config.throw).not.toBeCalled()
expect(config.next).toHaveBeenCalled() expect(config.next).toHaveBeenCalled()
expect(mockedGetRequiredResourceRole).toBeCalledTimes(1) expect(mockedGetResourcePerms).toBeCalledTimes(1)
expect(mockedGetRequiredResourceRole).toBeCalledWith( expect(mockedGetResourcePerms).toBeCalledWith(resourceId)
PermissionLevel.READ,
expect.objectContaining({
resourceId: tableId,
subResourceId: undefined,
})
)
})
it("will use view permissions if flag is on", async () => {
mocks.licenses.useViewPermissions()
await config.executeMiddleware()
expect(config.throw).not.toBeCalled()
expect(config.next).toHaveBeenCalled()
expect(mockedGetRequiredResourceRole).toBeCalledTimes(1)
expect(mockedGetRequiredResourceRole).toBeCalledWith(
PermissionLevel.READ,
expect.objectContaining({
resourceId: tableId,
subResourceId: viewId,
})
)
})
it("throw an exception if the resource id is not provided", async () => {
config.setResourceId(undefined)
await config.executeMiddleware()
expect(config.throw).toHaveBeenNthCalledWith(
1,
400,
"Cannot obtain the view id"
)
})
it("throw an exception if the resource id is not a valid view id", async () => {
config.setResourceId(tableId)
await config.executeMiddleware()
expect(config.throw).toHaveBeenNthCalledWith(
1,
400,
`"${tableId}" is not a valid view id`
)
}) })
}) })
}) })

View File

@ -60,7 +60,7 @@ function tarFilesToTmp(tmpDir: string, files: string[]) {
export async function exportDB( export async function exportDB(
dbName: string, dbName: string,
opts: DBDumpOpts = {} opts: DBDumpOpts = {}
): Promise<DBDumpOpts> { ): Promise<string> {
const exportOpts = { const exportOpts = {
filter: opts?.filter, filter: opts?.filter,
batch_size: 1000, batch_size: 1000,