diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index b86ec38d08..6490b4770a 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -14,6 +14,7 @@ import { InternalTable, tenancy, features, + utils, } from "@budibase/backend-core" import { quotas } from "@budibase/pro" import { @@ -757,6 +758,37 @@ describe.each([ }) }) + describe("user column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + default: "{{ [Current User]._id }}", + }, + }, + }) + ) + }) + + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.user._id).toEqual(config.getUser()._id) + }) + + it("does not use default value if value specified", async () => { + const id = `us_${utils.newid()}` + await config.createUser({ _id: id }) + const row = await config.api.row.save(table._id!, { + user: id, + }) + expect(row.user._id).toEqual(id) + }) + }) + describe("bindings", () => { describe("string column", () => { beforeAll(async () => { diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts index d6c1651ef0..f7fe6a66d4 100644 --- a/packages/server/src/api/routes/tests/table.spec.ts +++ b/packages/server/src/api/routes/tests/table.spec.ts @@ -21,6 +21,7 @@ import { ViewCalculation, ViewV2Enriched, RowExportFormat, + PermissionLevel, } from "@budibase/types" import { checkBuilderEndpoint } from "./utilities/TestFunctions" import * as setup from "./utilities" @@ -191,6 +192,55 @@ describe.each([ ) }) + describe("permissions", () => { + it("get the base permissions for the table", async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + name: { + type: FieldType.STRING, + name: "name", + }, + }, + }) + ) + + // get the explicit permissions + const { permissions } = await config.api.permission.get(table._id!, { + status: 200, + }) + const explicitPermissions = { + role: "ADMIN", + permissionType: "EXPLICIT", + } + expect(permissions.write).toEqual(explicitPermissions) + expect(permissions.read).toEqual(explicitPermissions) + + // revoke the explicit permissions + for (let level of [PermissionLevel.WRITE, PermissionLevel.READ]) { + await config.api.permission.revoke( + { + roleId: permissions[level].role, + resourceId: table._id!, + level, + }, + { status: 200 } + ) + } + + // check base permissions + const { permissions: basePermissions } = await config.api.permission.get( + table._id!, + { + status: 200, + } + ) + const basePerms = { role: "BASIC", permissionType: "BASE" } + expect(basePermissions.write).toEqual(basePerms) + expect(basePermissions.read).toEqual(basePerms) + }) + }) + describe("update", () => { it("updates a table", async () => { const table = await config.api.table.save( diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index eae429d49d..7979fac555 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -3546,6 +3546,50 @@ describe.each([ expect(response.rows).toHaveLength(1) expect(response.rows[0].sum).toEqual(61) }) + + it("should be able to filter on a single user field in both the view query and search query", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + }, + }, + }) + ) + + await config.api.row.save(table._id!, { + user: config.getUser()._id, + }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + query: { + equal: { + user: "{{ [user].[_id] }}", + }, + }, + schema: { + user: { + visible: true, + }, + }, + }) + + const { rows } = await config.api.viewV2.search(view.id, { + query: { + equal: { + user: "{{ [user].[_id] }}", + }, + }, + }) + + expect(rows).toHaveLength(1) + expect(rows[0].user._id).toEqual(config.getUser()._id) + }) }) describe("permissions", () => { diff --git a/packages/server/src/sdk/app/permissions/index.ts b/packages/server/src/sdk/app/permissions/index.ts index 97af9ccc83..7c385864f4 100644 --- a/packages/server/src/sdk/app/permissions/index.ts +++ b/packages/server/src/sdk/app/permissions/index.ts @@ -73,8 +73,7 @@ export async function getResourcePerms( p[level] = { role, type: PermissionSource.BASE } return p }, {}) - const result = Object.assign(basePermissions, permissions) - return result + return Object.assign(basePermissions, permissions) } export async function getDependantResources( diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index f68f78f0bc..7ac3bb8ead 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -16,7 +16,7 @@ import * as external from "./search/external" import { ExportRowsParams, ExportRowsResult } from "./search/types" import { dataFilters } from "@budibase/shared-core" import sdk from "../../index" -import { searchInputMapping } from "./search/utils" +import { checkFilters, searchInputMapping } from "./search/utils" import { db, features } from "@budibase/backend-core" import tracer from "dd-trace" import { getQueryableFields, removeInvalidFilters } from "./queryUtils" @@ -81,6 +81,10 @@ export async function search( options.query = {} } + if (context) { + options.query = await enrichSearchContext(options.query, context) + } + // need to make sure filters in correct shape before checking for view options = searchInputMapping(table, options) @@ -92,12 +96,15 @@ export async function search( // 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.buildQueryLegacy(view.query) || {} + let viewQuery = await enrichSearchContext(view.query || {}, context) + viewQuery = dataFilters.buildQueryLegacy(viewQuery) || {} + viewQuery = checkFilters(table, viewQuery) delete viewQuery?.onEmptyFilter const sqsEnabled = await features.flags.isEnabled("SQS") const supportsLogicalOperators = isExternalTableID(view.tableId) || sqsEnabled + if (!supportsLogicalOperators) { // In the unlikely event that a Grouped Filter is in a non-SQS environment // It needs to be ignored entirely @@ -113,13 +120,12 @@ export async function search( ?.filter(filter => filter.field) .map(filter => db.removeKeyNumbering(filter.field)) || [] - viewQuery ??= {} // Carry over filters for unused fields Object.keys(options.query).forEach(key => { const operator = key as Exclude Object.keys(options.query[operator] || {}).forEach(field => { if (!existingFields.includes(db.removeKeyNumbering(field))) { - viewQuery![operator]![field] = options.query[operator]![field] + viewQuery[operator]![field] = options.query[operator]![field] } }) }) @@ -137,10 +143,6 @@ export async function search( } } - if (context) { - options.query = await enrichSearchContext(options.query, context) - } - options.query = dataFilters.cleanupQuery(options.query) options.query = dataFilters.fixupFilterArrays(options.query) diff --git a/packages/server/src/sdk/app/rows/search/utils.ts b/packages/server/src/sdk/app/rows/search/utils.ts index 90303a6ca7..c7de7eef37 100644 --- a/packages/server/src/sdk/app/rows/search/utils.ts +++ b/packages/server/src/sdk/app/rows/search/utils.ts @@ -80,35 +80,41 @@ function userColumnMapping(column: string, filters: SearchFilters) { }) } +export function checkFilters( + table: Table, + filters: SearchFilters +): SearchFilters { + for (let [key, column] of Object.entries(table.schema || {})) { + switch (column.type) { + case FieldType.BB_REFERENCE_SINGLE: { + const subtype = column.subtype + switch (subtype) { + case BBReferenceFieldSubType.USER: + userColumnMapping(key, filters) + break + + default: + utils.unreachable(subtype) + } + break + } + case FieldType.BB_REFERENCE: { + userColumnMapping(key, filters) + break + } + } + } + return dataFilters.recurseLogicalOperators(filters, filters => + checkFilters(table, filters) + ) +} + // 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. export function searchInputMapping(table: Table, options: RowSearchParams) { // need an internal function to loop over filters, because this takes the full options - function checkFilters(filters: SearchFilters) { - for (let [key, column] of Object.entries(table.schema || {})) { - switch (column.type) { - case FieldType.BB_REFERENCE_SINGLE: { - const subtype = column.subtype - switch (subtype) { - case BBReferenceFieldSubType.USER: - userColumnMapping(key, filters) - break - - default: - utils.unreachable(subtype) - } - break - } - case FieldType.BB_REFERENCE: { - userColumnMapping(key, filters) - break - } - } - } - return dataFilters.recurseLogicalOperators(filters, checkFilters) - } if (options.query) { - options.query = checkFilters(options.query) + options.query = checkFilters(table, options.query) } return options } diff --git a/packages/server/src/utilities/security.ts b/packages/server/src/utilities/security.ts index 4f93c33ee4..7cfbeb197c 100644 --- a/packages/server/src/utilities/security.ts +++ b/packages/server/src/utilities/security.ts @@ -1,5 +1,5 @@ -import { permissions, roles } from "@budibase/backend-core" -import { DocumentType, VirtualDocumentType } from "../db/utils" +import { DocumentType, permissions, roles } from "@budibase/backend-core" +import { VirtualDocumentType } from "../db/utils" import { getDocumentType, getVirtualDocumentType } from "@budibase/types" export const CURRENTLY_SUPPORTED_LEVELS: string[] = [ @@ -19,6 +19,7 @@ export function getPermissionType(resourceId: string) { switch (docType) { case DocumentType.TABLE: case DocumentType.ROW: + case DocumentType.DATASOURCE_PLUS: return permissions.PermissionType.TABLE case DocumentType.AUTOMATION: return permissions.PermissionType.AUTOMATION diff --git a/packages/shared-core/src/table.ts b/packages/shared-core/src/table.ts index 57f6854604..677b1e2357 100644 --- a/packages/shared-core/src/table.ts +++ b/packages/shared-core/src/table.ts @@ -67,7 +67,7 @@ const allowDefaultColumnByType: Record = { [FieldType.SIGNATURE_SINGLE]: false, [FieldType.LINK]: false, [FieldType.BB_REFERENCE]: false, - [FieldType.BB_REFERENCE_SINGLE]: false, + [FieldType.BB_REFERENCE_SINGLE]: true, } export function canBeDisplayColumn(type: FieldType): boolean { diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts index f9d1a4c012..f5bb081fd5 100644 --- a/packages/types/src/documents/app/table/schema.ts +++ b/packages/types/src/documents/app/table/schema.ts @@ -126,6 +126,7 @@ export interface BBReferenceSingleFieldMetadata extends Omit { type: FieldType.BB_REFERENCE_SINGLE subtype: Exclude + default?: string } export interface AttachmentFieldMetadata extends BaseFieldSchema {