Merge branch 'master' of github.com:budibase/budibase into view-calculation-sql-2
This commit is contained in:
commit
3dbda132b8
|
@ -1,3 +1,3 @@
|
||||||
nodejs 20.10.0
|
nodejs 20.10.0
|
||||||
python 3.10.0
|
python 3.10.0
|
||||||
yarn 1.22.19
|
yarn 1.22.22
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.32.8",
|
"version": "2.32.10",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ef8690c955cb83768f21770ece72d68cccddc8a9
|
Subproject commit 3e24f6293ff5ee5f9b42822e001504e3bbf19cc0
|
|
@ -17,11 +17,8 @@ import {
|
||||||
ContextUser,
|
ContextUser,
|
||||||
CouchFindOptions,
|
CouchFindOptions,
|
||||||
DatabaseQueryOpts,
|
DatabaseQueryOpts,
|
||||||
SearchFilters,
|
|
||||||
SearchUsersRequest,
|
SearchUsersRequest,
|
||||||
User,
|
User,
|
||||||
BasicOperator,
|
|
||||||
ArrayOperator,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { getGlobalDB } from "../context"
|
import { getGlobalDB } from "../context"
|
||||||
|
@ -45,32 +42,6 @@ function removeUserPassword(users: User | User[]) {
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSupportedUserSearch(query: SearchFilters) {
|
|
||||||
const allowed = [
|
|
||||||
{ op: BasicOperator.STRING, key: "email" },
|
|
||||||
{ op: BasicOperator.EQUAL, key: "_id" },
|
|
||||||
{ op: ArrayOperator.ONE_OF, key: "_id" },
|
|
||||||
]
|
|
||||||
for (let [key, operation] of Object.entries(query)) {
|
|
||||||
if (typeof operation !== "object") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const fields = Object.keys(operation || {})
|
|
||||||
// this filter doesn't contain options - ignore
|
|
||||||
if (fields.length === 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const allowedOperation = allowed.find(
|
|
||||||
allow =>
|
|
||||||
allow.op === key && fields.length === 1 && fields[0] === allow.key
|
|
||||||
)
|
|
||||||
if (!allowedOperation) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bulkGetGlobalUsersById(
|
export async function bulkGetGlobalUsersById(
|
||||||
userIds: string[],
|
userIds: string[],
|
||||||
opts?: GetOpts
|
opts?: GetOpts
|
||||||
|
|
|
@ -641,6 +641,8 @@
|
||||||
let hasUserDefinedName = automation.stepNames?.[allSteps[idx]?.id]
|
let hasUserDefinedName = automation.stepNames?.[allSteps[idx]?.id]
|
||||||
if (isLoopBlock) {
|
if (isLoopBlock) {
|
||||||
runtimeName = `loop.${name}`
|
runtimeName = `loop.${name}`
|
||||||
|
} else if (idx === 0) {
|
||||||
|
runtimeName = `trigger.${name}`
|
||||||
} else if (block.name.startsWith("JS")) {
|
} else if (block.name.startsWith("JS")) {
|
||||||
runtimeName = hasUserDefinedName
|
runtimeName = hasUserDefinedName
|
||||||
? `stepsByName["${bindingName}"].${name}`
|
? `stepsByName["${bindingName}"].${name}`
|
||||||
|
@ -650,7 +652,7 @@
|
||||||
? `stepsByName.${bindingName}.${name}`
|
? `stepsByName.${bindingName}.${name}`
|
||||||
: `steps.${idx - loopBlockCount}.${name}`
|
: `steps.${idx - loopBlockCount}.${name}`
|
||||||
}
|
}
|
||||||
return idx === 0 ? `trigger.${name}` : runtimeName
|
return runtimeName
|
||||||
}
|
}
|
||||||
|
|
||||||
const determineCategoryName = (idx, isLoopBlock, bindingName) => {
|
const determineCategoryName = (idx, isLoopBlock, bindingName) => {
|
||||||
|
@ -677,7 +679,7 @@
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
readableBinding:
|
readableBinding:
|
||||||
bindingName && !isLoopBlock
|
bindingName && !isLoopBlock && idx !== 0
|
||||||
? `steps.${bindingName}.${name}`
|
? `steps.${bindingName}.${name}`
|
||||||
: runtimeBinding,
|
: runtimeBinding,
|
||||||
runtimeBinding,
|
runtimeBinding,
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { permissions, roles, context } from "@budibase/backend-core"
|
import { permissions, roles, context } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
UserCtx,
|
UserCtx,
|
||||||
Database,
|
|
||||||
Role,
|
Role,
|
||||||
PermissionLevel,
|
|
||||||
GetResourcePermsResponse,
|
GetResourcePermsResponse,
|
||||||
ResourcePermissionInfo,
|
ResourcePermissionInfo,
|
||||||
GetDependantResourcesResponse,
|
GetDependantResourcesResponse,
|
||||||
|
@ -12,107 +10,15 @@ import {
|
||||||
RemovePermissionRequest,
|
RemovePermissionRequest,
|
||||||
RemovePermissionResponse,
|
RemovePermissionResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
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 sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
import { PermissionUpdateType } from "../../sdk/app/permissions"
|
||||||
const enum PermissionUpdateType {
|
|
||||||
REMOVE = "remove",
|
|
||||||
ADD = "add",
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
||||||
|
|
||||||
// utility function to stop this repetition - permissions always stored under roles
|
|
||||||
async function getAllDBRoles(db: Database) {
|
|
||||||
const body = await db.allDocs<Role>(
|
|
||||||
getRoleParams(null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return body.rows.map(row => row.doc!)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updatePermissionOnRole(
|
|
||||||
{
|
|
||||||
roleId,
|
|
||||||
resourceId,
|
|
||||||
level,
|
|
||||||
}: { roleId: string; resourceId: string; level: PermissionLevel },
|
|
||||||
updateType: PermissionUpdateType
|
|
||||||
) {
|
|
||||||
const db = context.getAppDB()
|
|
||||||
const remove = updateType === PermissionUpdateType.REMOVE
|
|
||||||
const isABuiltin = roles.isBuiltin(roleId)
|
|
||||||
const dbRoleId = roles.getDBRoleID(roleId)
|
|
||||||
const dbRoles = await getAllDBRoles(db)
|
|
||||||
const docUpdates: Role[] = []
|
|
||||||
|
|
||||||
// the permission is for a built in, make sure it exists
|
|
||||||
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
|
|
||||||
const builtin = roles.getBuiltinRoles()[roleId]
|
|
||||||
builtin._id = roles.getDBRoleID(builtin._id!)
|
|
||||||
dbRoles.push(builtin)
|
|
||||||
}
|
|
||||||
|
|
||||||
// now try to find any roles which need updated, e.g. removing the
|
|
||||||
// resource from another role and then adding to the new role
|
|
||||||
for (let role of dbRoles) {
|
|
||||||
let updated = false
|
|
||||||
const rolePermissions: Record<string, PermissionLevel[]> = role.permissions
|
|
||||||
? role.permissions
|
|
||||||
: {}
|
|
||||||
// make sure its an array, also handle migrating
|
|
||||||
if (
|
|
||||||
!rolePermissions[resourceId] ||
|
|
||||||
!Array.isArray(rolePermissions[resourceId])
|
|
||||||
) {
|
|
||||||
rolePermissions[resourceId] =
|
|
||||||
typeof rolePermissions[resourceId] === "string"
|
|
||||||
? [rolePermissions[resourceId] as unknown as PermissionLevel]
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
// handle the removal/updating the role which has this permission first
|
|
||||||
// the updating (role._id !== dbRoleId) is required because a resource/level can
|
|
||||||
// only be permitted in a single role (this reduces hierarchy confusion and simplifies
|
|
||||||
// the general UI for this, rather than needing to show everywhere it is used)
|
|
||||||
if (
|
|
||||||
(role._id !== dbRoleId || remove) &&
|
|
||||||
rolePermissions[resourceId].indexOf(level) !== -1
|
|
||||||
) {
|
|
||||||
removeFromArray(rolePermissions[resourceId], level)
|
|
||||||
updated = true
|
|
||||||
}
|
|
||||||
// handle the adding, we're on the correct role, at it to this
|
|
||||||
if (!remove && role._id === dbRoleId) {
|
|
||||||
const set = new Set(rolePermissions[resourceId])
|
|
||||||
rolePermissions[resourceId] = [...set.add(level)]
|
|
||||||
updated = true
|
|
||||||
}
|
|
||||||
// handle the update, add it to bulk docs to perform at end
|
|
||||||
if (updated) {
|
|
||||||
role.permissions = rolePermissions
|
|
||||||
docUpdates.push(role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await db.bulkDocs(docUpdates)
|
|
||||||
return response.map(resp => {
|
|
||||||
const version = docUpdates.find(role => role._id === resp.id)?.version
|
|
||||||
const _id = roles.getExternalRoleID(resp.id, version)
|
|
||||||
return {
|
|
||||||
_id,
|
|
||||||
rev: resp.rev,
|
|
||||||
error: resp.error,
|
|
||||||
reason: resp.reason,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchBuiltin(ctx: UserCtx) {
|
export function fetchBuiltin(ctx: UserCtx) {
|
||||||
ctx.body = Object.values(permissions.getBuiltinPermissions())
|
ctx.body = Object.values(permissions.getBuiltinPermissions())
|
||||||
}
|
}
|
||||||
|
@ -124,7 +30,7 @@ export function fetchLevels(ctx: UserCtx) {
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const dbRoles: Role[] = await getAllDBRoles(db)
|
const dbRoles: Role[] = await sdk.permissions.getAllDBRoles(db)
|
||||||
let permissions: any = {}
|
let permissions: any = {}
|
||||||
// create an object with structure role ID -> resource ID -> level
|
// create an object with structure role ID -> resource ID -> level
|
||||||
for (let role of dbRoles) {
|
for (let role of dbRoles) {
|
||||||
|
@ -186,12 +92,18 @@ export async function getDependantResources(
|
||||||
|
|
||||||
export async function addPermission(ctx: UserCtx<void, AddPermissionResponse>) {
|
export async function addPermission(ctx: UserCtx<void, AddPermissionResponse>) {
|
||||||
const params: AddPermissionRequest = ctx.params
|
const params: AddPermissionRequest = ctx.params
|
||||||
ctx.body = await updatePermissionOnRole(params, PermissionUpdateType.ADD)
|
ctx.body = await sdk.permissions.updatePermissionOnRole(
|
||||||
|
params,
|
||||||
|
PermissionUpdateType.ADD
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removePermission(
|
export async function removePermission(
|
||||||
ctx: UserCtx<void, RemovePermissionResponse>
|
ctx: UserCtx<void, RemovePermissionResponse>
|
||||||
) {
|
) {
|
||||||
const params: RemovePermissionRequest = ctx.params
|
const params: RemovePermissionRequest = ctx.params
|
||||||
ctx.body = await updatePermissionOnRole(params, PermissionUpdateType.REMOVE)
|
ctx.body = await sdk.permissions.updatePermissionOnRole(
|
||||||
|
params,
|
||||||
|
PermissionUpdateType.REMOVE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,9 @@ import {
|
||||||
ViewV2,
|
ViewV2,
|
||||||
SearchRowResponse,
|
SearchRowResponse,
|
||||||
SearchViewRowRequest,
|
SearchViewRowRequest,
|
||||||
SearchFilterKey,
|
|
||||||
LogicalOperator,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { db, context, features } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { enrichSearchContext } from "./utils"
|
|
||||||
import { isExternalTableID } from "../../../integrations/utils"
|
|
||||||
|
|
||||||
export async function searchView(
|
export async function searchView(
|
||||||
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
||||||
|
@ -27,58 +22,23 @@ export async function searchView(
|
||||||
|
|
||||||
const { body } = ctx.request
|
const { body } = ctx.request
|
||||||
|
|
||||||
// Enrich saved query with ephemeral query params.
|
|
||||||
// We prevent searching on any fields that are saved as part of the query, as
|
|
||||||
// that could let users find rows they should not be allowed to access.
|
|
||||||
let query = dataFilters.buildQuery(view.query || [])
|
|
||||||
if (body.query) {
|
|
||||||
// Delete extraneous search params that cannot be overridden
|
|
||||||
delete body.query.onEmptyFilter
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isExternalTableID(view.tableId) &&
|
|
||||||
!(await features.flags.isEnabled("SQS"))
|
|
||||||
) {
|
|
||||||
// Extract existing fields
|
|
||||||
const existingFields =
|
|
||||||
view.query
|
|
||||||
?.filter(filter => filter.field)
|
|
||||||
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
|
||||||
|
|
||||||
// Carry over filters for unused fields
|
|
||||||
Object.keys(body.query).forEach(key => {
|
|
||||||
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
|
||||||
Object.keys(body.query[operator] || {}).forEach(field => {
|
|
||||||
if (!existingFields.includes(db.removeKeyNumbering(field))) {
|
|
||||||
query[operator]![field] = body.query[operator]![field]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
query = {
|
|
||||||
$and: {
|
|
||||||
conditions: [query, body.query],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.ensureSnippetContext(true)
|
await context.ensureSnippetContext(true)
|
||||||
|
|
||||||
const enrichedQuery = await enrichSearchContext(query, {
|
const result = await sdk.rows.search(
|
||||||
user: sdk.users.getUserContextBindings(ctx.user),
|
{
|
||||||
})
|
viewId: view.id,
|
||||||
|
tableId: view.tableId,
|
||||||
const result = await sdk.rows.search({
|
query: body.query,
|
||||||
viewId: view.id,
|
...getSortOptions(body, view),
|
||||||
tableId: view.tableId,
|
limit: body.limit,
|
||||||
query: enrichedQuery,
|
bookmark: body.bookmark,
|
||||||
...getSortOptions(body, view),
|
paginate: body.paginate,
|
||||||
limit: body.limit,
|
countRows: body.countRows,
|
||||||
bookmark: body.bookmark,
|
},
|
||||||
paginate: body.paginate,
|
{
|
||||||
countRows: body.countRows,
|
user: sdk.users.getUserContextBindings(ctx.user),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
result.rows.forEach(r => (r._viewId = view.id))
|
result.rows.forEach(r => (r._viewId = view.id))
|
||||||
ctx.body = result
|
ctx.body = result
|
||||||
|
|
|
@ -125,6 +125,13 @@ describe("/permission", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to access the view data when the table is set to public and with no view permissions overrides", async () => {
|
it("should be able to access the view data when the table is set to public and with no view permissions overrides", async () => {
|
||||||
|
// Make view inherit table permissions. Needed for backwards compatibility with existing views.
|
||||||
|
await config.api.permission.revoke({
|
||||||
|
roleId: STD_ROLE_ID,
|
||||||
|
resourceId: view.id,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
|
||||||
// replicate changes before checking permissions
|
// replicate changes before checking permissions
|
||||||
await config.publish()
|
await config.publish()
|
||||||
|
|
||||||
|
@ -138,6 +145,14 @@ describe("/permission", () => {
|
||||||
resourceId: table._id,
|
resourceId: table._id,
|
||||||
level: PermissionLevel.READ,
|
level: PermissionLevel.READ,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Make view inherit table permissions. Needed for backwards compatibility with existing views.
|
||||||
|
await config.api.permission.revoke({
|
||||||
|
roleId: STD_ROLE_ID,
|
||||||
|
resourceId: view.id,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
|
||||||
// replicate changes before checking permissions
|
// replicate changes before checking permissions
|
||||||
await config.publish()
|
await config.publish()
|
||||||
|
|
||||||
|
|
|
@ -826,11 +826,20 @@ describe("/rowsActions", () => {
|
||||||
)
|
)
|
||||||
).id
|
).id
|
||||||
|
|
||||||
|
// Allow row action on view
|
||||||
await config.api.rowAction.setViewPermission(
|
await config.api.rowAction.setViewPermission(
|
||||||
tableId,
|
tableId,
|
||||||
viewId,
|
viewId,
|
||||||
rowAction.id
|
rowAction.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Delete explicit view permissions so they inherit table permissions
|
||||||
|
await config.api.permission.revoke({
|
||||||
|
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, // Don't think this matters since we are revoking the permission
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
resourceId: viewId,
|
||||||
|
})
|
||||||
|
|
||||||
return { permissionResource: tableId, triggerResouce: viewId }
|
return { permissionResource: tableId, triggerResouce: viewId }
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -408,7 +408,6 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// We've decided not to try and support binding for in-memory search just now.
|
|
||||||
!isInMemory &&
|
!isInMemory &&
|
||||||
describe("bindings", () => {
|
describe("bindings", () => {
|
||||||
let globalUsers: any = []
|
let globalUsers: any = []
|
||||||
|
@ -528,6 +527,20 @@ describe.each([
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
!isLucene &&
|
||||||
|
it("should return all rows matching the session user firstname when logical operator used", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
$and: {
|
||||||
|
conditions: [{ equal: { name: "{{ [user].firstName }}" } }],
|
||||||
|
},
|
||||||
|
}).toContainExactly([
|
||||||
|
{
|
||||||
|
name: config.getUser().firstName,
|
||||||
|
appointment: future.toISOString(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it("should parse the date binding and return all rows after the resolved value", async () => {
|
it("should parse the date binding and return all rows after the resolved value", async () => {
|
||||||
await tk.withFreeze(serverTime, async () => {
|
await tk.withFreeze(serverTime, async () => {
|
||||||
await expectQuery({
|
await expectQuery({
|
||||||
|
|
|
@ -1738,6 +1738,40 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("views filters are respected even if the column is hidden", async () => {
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
one: "foo",
|
||||||
|
two: "bar",
|
||||||
|
})
|
||||||
|
const two = await config.api.row.save(table._id!, {
|
||||||
|
one: "foo2",
|
||||||
|
two: "bar2",
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
query: [
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "two",
|
||||||
|
value: "bar2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
id: { visible: true },
|
||||||
|
one: { visible: false },
|
||||||
|
two: { visible: false },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.viewV2.search(view.id)
|
||||||
|
expect(response.rows).toHaveLength(1)
|
||||||
|
expect(response.rows).toEqual([
|
||||||
|
expect.objectContaining({ _id: two._id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it("views without data can be returned", async () => {
|
it("views without data can be returned", async () => {
|
||||||
const response = await config.api.viewV2.search(view.id)
|
const response = await config.api.viewV2.search(view.id)
|
||||||
expect(response.rows).toHaveLength(0)
|
expect(response.rows).toHaveLength(0)
|
||||||
|
@ -2460,6 +2494,11 @@ describe.each([
|
||||||
level: PermissionLevel.READ,
|
level: PermissionLevel.READ,
|
||||||
resourceId: table._id!,
|
resourceId: table._id!,
|
||||||
})
|
})
|
||||||
|
await config.api.permission.revoke({
|
||||||
|
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, // Don't think this matters since we are revoking the permission
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
resourceId: view.id,
|
||||||
|
})
|
||||||
await config.publish()
|
await config.publish()
|
||||||
|
|
||||||
const response = await config.api.viewV2.publicSearch(view.id)
|
const response = await config.api.viewV2.publicSearch(view.id)
|
||||||
|
|
|
@ -1,22 +1,30 @@
|
||||||
import { db, docIds, roles } from "@budibase/backend-core"
|
import { db, roles, context, docIds } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
PermissionSource,
|
PermissionSource,
|
||||||
VirtualDocumentType,
|
VirtualDocumentType,
|
||||||
|
Role,
|
||||||
|
Database,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { extractViewInfoFromID } from "../../../db/utils"
|
import { extractViewInfoFromID, getRoleParams } from "../../../db/utils"
|
||||||
import {
|
import {
|
||||||
CURRENTLY_SUPPORTED_LEVELS,
|
CURRENTLY_SUPPORTED_LEVELS,
|
||||||
getBasePermissions,
|
getBasePermissions,
|
||||||
} from "../../../utilities/security"
|
} from "../../../utilities/security"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { isV2 } from "../views"
|
import { isV2 } from "../views"
|
||||||
|
import { removeFromArray } from "../../../utilities"
|
||||||
|
|
||||||
type ResourcePermissions = Record<
|
type ResourcePermissions = Record<
|
||||||
string,
|
string,
|
||||||
{ role: string; type: PermissionSource }
|
{ role: string; type: PermissionSource }
|
||||||
>
|
>
|
||||||
|
|
||||||
|
export const enum PermissionUpdateType {
|
||||||
|
REMOVE = "remove",
|
||||||
|
ADD = "add",
|
||||||
|
}
|
||||||
|
|
||||||
export async function getInheritablePermissions(
|
export async function getInheritablePermissions(
|
||||||
resourceId: string
|
resourceId: string
|
||||||
): Promise<ResourcePermissions | undefined> {
|
): Promise<ResourcePermissions | undefined> {
|
||||||
|
@ -100,3 +108,89 @@ export async function getDependantResources(
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updatePermissionOnRole(
|
||||||
|
{
|
||||||
|
roleId,
|
||||||
|
resourceId,
|
||||||
|
level,
|
||||||
|
}: { roleId: string; resourceId: string; level: PermissionLevel },
|
||||||
|
updateType: PermissionUpdateType
|
||||||
|
) {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const remove = updateType === PermissionUpdateType.REMOVE
|
||||||
|
const isABuiltin = roles.isBuiltin(roleId)
|
||||||
|
const dbRoleId = roles.getDBRoleID(roleId)
|
||||||
|
const dbRoles = await getAllDBRoles(db)
|
||||||
|
const docUpdates: Role[] = []
|
||||||
|
|
||||||
|
// the permission is for a built in, make sure it exists
|
||||||
|
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
|
||||||
|
const builtin = roles.getBuiltinRoles()[roleId]
|
||||||
|
builtin._id = roles.getDBRoleID(builtin._id!)
|
||||||
|
dbRoles.push(builtin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now try to find any roles which need updated, e.g. removing the
|
||||||
|
// resource from another role and then adding to the new role
|
||||||
|
for (let role of dbRoles) {
|
||||||
|
let updated = false
|
||||||
|
const rolePermissions: Record<string, PermissionLevel[]> = role.permissions
|
||||||
|
? role.permissions
|
||||||
|
: {}
|
||||||
|
// make sure its an array, also handle migrating
|
||||||
|
if (
|
||||||
|
!rolePermissions[resourceId] ||
|
||||||
|
!Array.isArray(rolePermissions[resourceId])
|
||||||
|
) {
|
||||||
|
rolePermissions[resourceId] =
|
||||||
|
typeof rolePermissions[resourceId] === "string"
|
||||||
|
? [rolePermissions[resourceId] as unknown as PermissionLevel]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
// handle the removal/updating the role which has this permission first
|
||||||
|
// the updating (role._id !== dbRoleId) is required because a resource/level can
|
||||||
|
// only be permitted in a single role (this reduces hierarchy confusion and simplifies
|
||||||
|
// the general UI for this, rather than needing to show everywhere it is used)
|
||||||
|
if (
|
||||||
|
(role._id !== dbRoleId || remove) &&
|
||||||
|
rolePermissions[resourceId].indexOf(level) !== -1
|
||||||
|
) {
|
||||||
|
removeFromArray(rolePermissions[resourceId], level)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
// handle the adding, we're on the correct role, at it to this
|
||||||
|
if (!remove && role._id === dbRoleId) {
|
||||||
|
const set = new Set(rolePermissions[resourceId])
|
||||||
|
rolePermissions[resourceId] = [...set.add(level)]
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
// handle the update, add it to bulk docs to perform at end
|
||||||
|
if (updated) {
|
||||||
|
role.permissions = rolePermissions
|
||||||
|
docUpdates.push(role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await db.bulkDocs(docUpdates)
|
||||||
|
return response.map(resp => {
|
||||||
|
const version = docUpdates.find(role => role._id === resp.id)?.version
|
||||||
|
const _id = roles.getExternalRoleID(resp.id, version)
|
||||||
|
return {
|
||||||
|
_id,
|
||||||
|
rev: resp.rev,
|
||||||
|
error: resp.error,
|
||||||
|
reason: resp.reason,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility function to stop this repetition - permissions always stored under roles
|
||||||
|
export async function getAllDBRoles(db: Database) {
|
||||||
|
const body = await db.allDocs<Role>(
|
||||||
|
getRoleParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return body.rows.map(row => row.doc!)
|
||||||
|
}
|
||||||
|
|
|
@ -16,11 +16,11 @@ export const removeInvalidFilters = (
|
||||||
|
|
||||||
validFields = validFields.map(f => f.toLowerCase())
|
validFields = validFields.map(f => f.toLowerCase())
|
||||||
for (const filterKey of Object.keys(result) as (keyof SearchFilters)[]) {
|
for (const filterKey of Object.keys(result) as (keyof SearchFilters)[]) {
|
||||||
const filter = result[filterKey]
|
|
||||||
if (!filter || typeof filter !== "object") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (isLogicalSearchOperator(filterKey)) {
|
if (isLogicalSearchOperator(filterKey)) {
|
||||||
|
const filter = result[filterKey]
|
||||||
|
if (!filter || typeof filter !== "object") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const resultingConditions: SearchFilters[] = []
|
const resultingConditions: SearchFilters[] = []
|
||||||
for (const condition of filter.conditions) {
|
for (const condition of filter.conditions) {
|
||||||
const resultingCondition = removeInvalidFilters(condition, validFields)
|
const resultingCondition = removeInvalidFilters(condition, validFields)
|
||||||
|
@ -36,6 +36,11 @@ export const removeInvalidFilters = (
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filter = result[filterKey]
|
||||||
|
if (!filter || typeof filter !== "object") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for (const columnKey of Object.keys(filter)) {
|
for (const columnKey of Object.keys(filter)) {
|
||||||
const possibleKeys = [columnKey, db.removeKeyNumbering(columnKey)].map(
|
const possibleKeys = [columnKey, db.removeKeyNumbering(columnKey)].map(
|
||||||
c => c.toLowerCase()
|
c => c.toLowerCase()
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import {
|
import {
|
||||||
EmptyFilterOption,
|
EmptyFilterOption,
|
||||||
|
LogicalOperator,
|
||||||
Row,
|
Row,
|
||||||
RowSearchParams,
|
RowSearchParams,
|
||||||
|
SearchFilterKey,
|
||||||
|
SearchFilters,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
Table,
|
Table,
|
||||||
|
@ -14,9 +17,10 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
import sdk from "../../index"
|
import sdk from "../../index"
|
||||||
import { searchInputMapping } from "./search/utils"
|
import { searchInputMapping } from "./search/utils"
|
||||||
import { features } from "@budibase/backend-core"
|
import { db, features } from "@budibase/backend-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
||||||
|
import { enrichSearchContext } from "../../../api/controllers/row/utils"
|
||||||
|
|
||||||
export { isValidFilter } from "../../../integrations/utils"
|
export { isValidFilter } from "../../../integrations/utils"
|
||||||
|
|
||||||
|
@ -34,7 +38,8 @@ function pickApi(tableId: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(
|
export async function search(
|
||||||
options: RowSearchParams
|
options: RowSearchParams,
|
||||||
|
context?: Record<string, any>
|
||||||
): Promise<SearchResponse<Row>> {
|
): Promise<SearchResponse<Row>> {
|
||||||
return await tracer.trace("search", async span => {
|
return await tracer.trace("search", async span => {
|
||||||
span?.addTags({
|
span?.addTags({
|
||||||
|
@ -51,7 +56,73 @@ export async function search(
|
||||||
countRows: options.countRows,
|
countRows: options.countRows,
|
||||||
})
|
})
|
||||||
|
|
||||||
options.query = dataFilters.cleanupQuery(options.query || {})
|
let source: Table | ViewV2
|
||||||
|
let table: Table
|
||||||
|
if (options.viewId) {
|
||||||
|
source = await sdk.views.get(options.viewId)
|
||||||
|
table = await sdk.views.getTable(source)
|
||||||
|
options = searchInputMapping(table, options)
|
||||||
|
} else if (options.tableId) {
|
||||||
|
source = await sdk.tables.getTable(options.tableId)
|
||||||
|
table = source
|
||||||
|
} else {
|
||||||
|
throw new Error(`Must supply either a view ID or a table ID`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExternalTable = isExternalTableID(table._id!)
|
||||||
|
|
||||||
|
if (options.query) {
|
||||||
|
const visibleFields = (
|
||||||
|
options.fields || Object.keys(table.schema)
|
||||||
|
).filter(field => table.schema[field].visible !== false)
|
||||||
|
|
||||||
|
const queryableFields = await getQueryableFields(table, visibleFields)
|
||||||
|
options.query = removeInvalidFilters(options.query, queryableFields)
|
||||||
|
} else {
|
||||||
|
options.query = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.viewId) {
|
||||||
|
const view = await sdk.views.get(options.viewId)
|
||||||
|
// Enrich saved query with ephemeral query params.
|
||||||
|
// We prevent searching on any fields that are saved as part of the query, as
|
||||||
|
// that could let users find rows they should not be allowed to access.
|
||||||
|
let viewQuery = dataFilters.buildQuery(view.query || [])
|
||||||
|
|
||||||
|
if (!isExternalTable && !(await features.flags.isEnabled("SQS"))) {
|
||||||
|
// Lucene does not accept conditional filters, so we need to keep the old logic
|
||||||
|
const query: SearchFilters = viewQuery
|
||||||
|
|
||||||
|
// Extract existing fields
|
||||||
|
const existingFields =
|
||||||
|
view.query
|
||||||
|
?.filter(filter => filter.field)
|
||||||
|
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
||||||
|
|
||||||
|
// Carry over filters for unused fields
|
||||||
|
Object.keys(options.query || {}).forEach(key => {
|
||||||
|
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
||||||
|
Object.keys(options.query[operator] || {}).forEach(field => {
|
||||||
|
if (!existingFields.includes(db.removeKeyNumbering(field))) {
|
||||||
|
query[operator]![field] = options.query[operator]![field]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
options.query = query
|
||||||
|
} else {
|
||||||
|
options.query = {
|
||||||
|
$and: {
|
||||||
|
conditions: [viewQuery, options.query],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
options.query = await enrichSearchContext(options.query, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
options.query = dataFilters.cleanupQuery(options.query)
|
||||||
options.query = dataFilters.fixupFilterArrays(options.query)
|
options.query = dataFilters.fixupFilterArrays(options.query)
|
||||||
|
|
||||||
span.addTags({
|
span.addTags({
|
||||||
|
@ -72,30 +143,8 @@ export async function search(
|
||||||
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
|
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
let source: Table | ViewV2
|
options = searchInputMapping(table, options)
|
||||||
let table: Table
|
|
||||||
if (options.viewId) {
|
|
||||||
source = await sdk.views.get(options.viewId)
|
|
||||||
table = await sdk.views.getTable(source)
|
|
||||||
options = searchInputMapping(table, options)
|
|
||||||
} else if (options.tableId) {
|
|
||||||
source = await sdk.tables.getTable(options.tableId)
|
|
||||||
table = source
|
|
||||||
options = searchInputMapping(table, options)
|
|
||||||
} else {
|
|
||||||
throw new Error(`Must supply either a view ID or a table ID`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.query) {
|
|
||||||
const visibleFields = (
|
|
||||||
options.fields || Object.keys(table.schema)
|
|
||||||
).filter(field => table.schema[field].visible !== false)
|
|
||||||
|
|
||||||
const queryableFields = await getQueryableFields(table, visibleFields)
|
|
||||||
options.query = removeInvalidFilters(options.query, queryableFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isExternalTable = isExternalTableID(table._id!)
|
|
||||||
let result: SearchResponse<Row>
|
let result: SearchResponse<Row>
|
||||||
if (isExternalTable) {
|
if (isExternalTable) {
|
||||||
span?.addTags({ searchType: "external" })
|
span?.addTags({ searchType: "external" })
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
RowSearchParams,
|
RowSearchParams,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { db as dbCore, context } from "@budibase/backend-core"
|
import { db as dbCore, context } from "@budibase/backend-core"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils, dataFilters } from "@budibase/shared-core"
|
||||||
|
|
||||||
export async function paginatedSearch(
|
export async function paginatedSearch(
|
||||||
query: SearchFilters,
|
query: SearchFilters,
|
||||||
|
@ -31,13 +31,13 @@ export async function fullSearch(
|
||||||
|
|
||||||
function findColumnInQueries(
|
function findColumnInQueries(
|
||||||
column: string,
|
column: string,
|
||||||
options: RowSearchParams,
|
filters: SearchFilters,
|
||||||
callback: (filter: any) => any
|
callback: (filter: any) => any
|
||||||
) {
|
) {
|
||||||
if (!options.query) {
|
if (!filters) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (let filterBlock of Object.values(options.query)) {
|
for (let filterBlock of Object.values(filters)) {
|
||||||
if (typeof filterBlock !== "object") {
|
if (typeof filterBlock !== "object") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -49,8 +49,8 @@ function findColumnInQueries(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function userColumnMapping(column: string, options: RowSearchParams) {
|
function userColumnMapping(column: string, filters: SearchFilters) {
|
||||||
findColumnInQueries(column, options, (filterValue: any): any => {
|
findColumnInQueries(column, filters, (filterValue: any): any => {
|
||||||
const isArray = Array.isArray(filterValue),
|
const isArray = Array.isArray(filterValue),
|
||||||
isString = typeof filterValue === "string"
|
isString = typeof filterValue === "string"
|
||||||
if (!isString && !isArray) {
|
if (!isString && !isArray) {
|
||||||
|
@ -83,26 +83,31 @@ function userColumnMapping(column: string, options: RowSearchParams) {
|
||||||
// maps through the search parameters to check if any of the inputs are invalid
|
// maps through the search parameters to check if any of the inputs are invalid
|
||||||
// based on the table schema, converts them to something that is valid.
|
// based on the table schema, converts them to something that is valid.
|
||||||
export function searchInputMapping(table: Table, options: RowSearchParams) {
|
export function searchInputMapping(table: Table, options: RowSearchParams) {
|
||||||
for (let [key, column] of Object.entries(table.schema || {})) {
|
// need an internal function to loop over filters, because this takes the full options
|
||||||
switch (column.type) {
|
function checkFilters(filters: SearchFilters) {
|
||||||
case FieldType.BB_REFERENCE_SINGLE: {
|
for (let [key, column] of Object.entries(table.schema || {})) {
|
||||||
const subtype = column.subtype
|
switch (column.type) {
|
||||||
switch (subtype) {
|
case FieldType.BB_REFERENCE_SINGLE: {
|
||||||
case BBReferenceFieldSubType.USER:
|
const subtype = column.subtype
|
||||||
userColumnMapping(key, options)
|
switch (subtype) {
|
||||||
break
|
case BBReferenceFieldSubType.USER:
|
||||||
|
userColumnMapping(key, filters)
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
utils.unreachable(subtype)
|
utils.unreachable(subtype)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case FieldType.BB_REFERENCE: {
|
||||||
|
userColumnMapping(key, filters)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
|
||||||
case FieldType.BB_REFERENCE: {
|
|
||||||
userColumnMapping(key, options)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return dataFilters.recurseLogicalOperators(filters, checkFilters)
|
||||||
}
|
}
|
||||||
|
options.query = checkFilters(options.query)
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
FieldType,
|
FieldType,
|
||||||
|
PermissionLevel,
|
||||||
RelationSchemaField,
|
RelationSchemaField,
|
||||||
RenameColumn,
|
RenameColumn,
|
||||||
Table,
|
Table,
|
||||||
|
@ -9,7 +10,7 @@ import {
|
||||||
ViewV2ColumnEnriched,
|
ViewV2ColumnEnriched,
|
||||||
ViewV2Enriched,
|
ViewV2Enriched,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { context, docIds, HTTPError } from "@budibase/backend-core"
|
import { context, docIds, HTTPError, roles } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
helpers,
|
helpers,
|
||||||
PROTECTED_EXTERNAL_COLUMNS,
|
PROTECTED_EXTERNAL_COLUMNS,
|
||||||
|
@ -22,6 +23,7 @@ import { isExternalTableID } from "../../../integrations/utils"
|
||||||
import * as internal from "./internal"
|
import * as internal from "./internal"
|
||||||
import * as external from "./external"
|
import * as external from "./external"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
import { PermissionUpdateType, updatePermissionOnRole } from "../permissions"
|
||||||
|
|
||||||
function pickApi(tableId: any) {
|
function pickApi(tableId: any) {
|
||||||
if (isExternalTableID(tableId)) {
|
if (isExternalTableID(tableId)) {
|
||||||
|
@ -191,7 +193,30 @@ export async function create(
|
||||||
): Promise<ViewV2> {
|
): Promise<ViewV2> {
|
||||||
await guardViewSchema(tableId, viewRequest)
|
await guardViewSchema(tableId, viewRequest)
|
||||||
|
|
||||||
return pickApi(tableId).create(tableId, viewRequest)
|
const view = await pickApi(tableId).create(tableId, viewRequest)
|
||||||
|
|
||||||
|
// Set permissions to be the same as the table
|
||||||
|
const tablePerms = await sdk.permissions.getResourcePerms(tableId)
|
||||||
|
const readRole = tablePerms[PermissionLevel.READ]?.role
|
||||||
|
const writeRole = tablePerms[PermissionLevel.WRITE]?.role
|
||||||
|
await updatePermissionOnRole(
|
||||||
|
{
|
||||||
|
roleId: readRole || roles.BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
resourceId: view.id,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
},
|
||||||
|
PermissionUpdateType.ADD
|
||||||
|
)
|
||||||
|
await updatePermissionOnRole(
|
||||||
|
{
|
||||||
|
roleId: writeRole || roles.BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
resourceId: view.id,
|
||||||
|
level: PermissionLevel.WRITE,
|
||||||
|
},
|
||||||
|
PermissionUpdateType.ADD
|
||||||
|
)
|
||||||
|
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(tableId: string, view: ViewV2): Promise<ViewV2> {
|
export async function update(tableId: string, view: ViewV2): Promise<ViewV2> {
|
||||||
|
|
|
@ -130,6 +130,26 @@ export function getUserContextBindings(user: ContextUser) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Current user context for bindable search
|
// Current user context for bindable search
|
||||||
const { _id, _rev, firstName, lastName, email, status, roleId } = user
|
const {
|
||||||
return { _id, _rev, firstName, lastName, email, status, roleId }
|
_id,
|
||||||
|
_rev,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
status,
|
||||||
|
roleId,
|
||||||
|
globalId,
|
||||||
|
userId,
|
||||||
|
} = user
|
||||||
|
return {
|
||||||
|
_id,
|
||||||
|
_rev,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
status,
|
||||||
|
roleId,
|
||||||
|
globalId,
|
||||||
|
userId,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -306,10 +306,12 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is similar to the outputProcessing function above, it makes sure that all the provided
|
* This function is similar to the outputProcessing function above, it makes
|
||||||
* rows are ready for output, but does not have enrichment for squash capabilities which can cause performance issues.
|
* sure that all the provided rows are ready for output, but does not have
|
||||||
* outputProcessing should be used when responding from the API, while this should be used when internally processing
|
* enrichment for squash capabilities which can cause performance issues.
|
||||||
* rows for any reason (like part of view operations).
|
* outputProcessing should be used when responding from the API, while this
|
||||||
|
* should be used when internally processing rows for any reason (like part of
|
||||||
|
* view operations).
|
||||||
*/
|
*/
|
||||||
export async function coreOutputProcessing(
|
export async function coreOutputProcessing(
|
||||||
source: Table | ViewV2,
|
source: Table | ViewV2,
|
||||||
|
|
|
@ -124,7 +124,7 @@ export function recurseLogicalOperators(
|
||||||
fn: (f: SearchFilters) => SearchFilters
|
fn: (f: SearchFilters) => SearchFilters
|
||||||
) {
|
) {
|
||||||
for (const logical of LOGICAL_OPERATORS) {
|
for (const logical of LOGICAL_OPERATORS) {
|
||||||
if (filters[logical]) {
|
if (filters?.[logical]) {
|
||||||
filters[logical]!.conditions = filters[logical]!.conditions.map(
|
filters[logical]!.conditions = filters[logical]!.conditions.map(
|
||||||
condition => fn(condition)
|
condition => fn(condition)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ArrayOperator, BasicOperator, SearchFilters } from "@budibase/types"
|
||||||
import * as Constants from "./constants"
|
import * as Constants from "./constants"
|
||||||
|
|
||||||
export function unreachable(
|
export function unreachable(
|
||||||
|
@ -77,3 +78,29 @@ export function trimOtherProps(object: any, allowedProps: string[]) {
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSupportedUserSearch(query: SearchFilters) {
|
||||||
|
const allowed = [
|
||||||
|
{ op: BasicOperator.STRING, key: "email" },
|
||||||
|
{ op: BasicOperator.EQUAL, key: "_id" },
|
||||||
|
{ op: ArrayOperator.ONE_OF, key: "_id" },
|
||||||
|
]
|
||||||
|
for (let [key, operation] of Object.entries(query)) {
|
||||||
|
if (typeof operation !== "object") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const fields = Object.keys(operation || {})
|
||||||
|
// this filter doesn't contain options - ignore
|
||||||
|
if (fields.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const allowedOperation = allowed.find(
|
||||||
|
allow =>
|
||||||
|
allow.op === key && fields.length === 1 && fields[0] === allow.key
|
||||||
|
)
|
||||||
|
if (!allowedOperation) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -68,6 +68,8 @@ type RangeFilter = Record<
|
||||||
[InternalSearchFilterOperator.COMPLEX_ID_OPERATOR]?: never
|
[InternalSearchFilterOperator.COMPLEX_ID_OPERATOR]?: never
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogicalFilter = { conditions: SearchFilters[] }
|
||||||
|
|
||||||
export type AnySearchFilter = BasicFilter | ArrayFilter | RangeFilter
|
export type AnySearchFilter = BasicFilter | ArrayFilter | RangeFilter
|
||||||
|
|
||||||
export interface SearchFilters {
|
export interface SearchFilters {
|
||||||
|
@ -92,12 +94,8 @@ export interface SearchFilters {
|
||||||
// specific document type (such as just rows)
|
// specific document type (such as just rows)
|
||||||
documentType?: DocumentType
|
documentType?: DocumentType
|
||||||
|
|
||||||
[LogicalOperator.AND]?: {
|
[LogicalOperator.AND]?: LogicalFilter
|
||||||
conditions: SearchFilters[]
|
[LogicalOperator.OR]?: LogicalFilter
|
||||||
}
|
|
||||||
[LogicalOperator.OR]?: {
|
|
||||||
conditions: SearchFilters[]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchFilterKey = keyof Omit<
|
export type SearchFilterKey = keyof Omit<
|
||||||
|
|
|
@ -37,7 +37,7 @@ import {
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { checkAnyUserExists } from "../../../utilities/users"
|
import { checkAnyUserExists } from "../../../utilities/users"
|
||||||
import { isEmailConfigured } from "../../../utilities/email"
|
import { isEmailConfigured } from "../../../utilities/email"
|
||||||
import { BpmStatusKey, BpmStatusValue } from "@budibase/shared-core"
|
import { BpmStatusKey, BpmStatusValue, utils } from "@budibase/shared-core"
|
||||||
|
|
||||||
const MAX_USERS_UPLOAD_LIMIT = 1000
|
const MAX_USERS_UPLOAD_LIMIT = 1000
|
||||||
|
|
||||||
|
@ -256,7 +256,7 @@ export const search = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Validate we aren't trying to search on any illegal fields
|
// Validate we aren't trying to search on any illegal fields
|
||||||
if (!userSdk.core.isSupportedUserSearch(body.query)) {
|
if (!utils.isSupportedUserSearch(body.query)) {
|
||||||
ctx.throw(400, "Can only search by string.email, equal._id or oneOf._id")
|
ctx.throw(400, "Can only search by string.email, equal._id or oneOf._id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue