Merge pull request #11655 from Budibase/BUDI-7393/use_permissions_on_middleware
Use permission sdk on middleware
This commit is contained in:
commit
25b82dd75a
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,23 +181,26 @@ 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(() => {
|
||||||
config.setMiddlewareRequiredPermission(
|
config.setMiddlewareRequiredPermission(
|
||||||
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`
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue