Merge branch 'master' of github.com:budibase/budibase into sql-security

This commit is contained in:
Sam Rose 2024-10-23 14:45:51 +01:00
commit 26192515b3
No known key found for this signature in database
22 changed files with 179 additions and 35 deletions

View File

@ -83,7 +83,7 @@ function getPackageJsonFields(): {
if (isDev() && !isTest()) { if (isDev() && !isTest()) {
try { try {
const lerna = getParentFile("lerna.json") const lerna = getParentFile("lerna.json")
localVersion = lerna.version localVersion = `${lerna.version}+local`
} catch { } catch {
// //
} }

View File

@ -1,4 +1,5 @@
<script> <script>
import { goto } from "@roxi/routify"
import { automationStore } from "stores/builder" import { automationStore } from "stores/builder"
import { import {
notifications, notifications,
@ -32,11 +33,12 @@
triggerVal.stepId, triggerVal.stepId,
triggerVal triggerVal
) )
await automationStore.actions.create(name, trigger) const automation = await automationStore.actions.create(name, trigger)
if (triggerVal.stepId === TriggerStepID.WEBHOOK) { if (triggerVal.stepId === TriggerStepID.WEBHOOK) {
webhookModal.show() webhookModal.show()
} }
notifications.success(`Automation ${name} created`) notifications.success(`Automation ${name} created`)
$goto(`../automation/${automation._id}`)
} catch (error) { } catch (error) {
notifications.error("Error creating automation") notifications.error("Error creating automation")
} }

View File

@ -159,6 +159,7 @@ export async function trigger(ctx: UserCtx) {
automation, automation,
{ {
fields: ctx.request.body.fields, fields: ctx.request.body.fields,
user: sdk.users.getUserContextBindings(ctx.user),
timeout: timeout:
ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT, ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT,
}, },
@ -183,6 +184,7 @@ export async function trigger(ctx: UserCtx) {
await triggers.externalTrigger(automation, { await triggers.externalTrigger(automation, {
...ctx.request.body, ...ctx.request.body,
appId: ctx.appId, appId: ctx.appId,
user: sdk.users.getUserContextBindings(ctx.user),
}) })
ctx.body = { ctx.body = {
message: `Automation ${automation._id} has been triggered.`, message: `Automation ${automation._id} has been triggered.`,
@ -212,6 +214,7 @@ export async function test(ctx: UserCtx) {
{ {
...testInput, ...testInput,
appId: ctx.appId, appId: ctx.appId,
user: sdk.users.getUserContextBindings(ctx.user),
}, },
{ getResponses: true } { getResponses: true }
) )

View File

@ -65,7 +65,14 @@ export async function patch(
} }
ctx.status = 200 ctx.status = 200
ctx.eventEmitter && ctx.eventEmitter &&
ctx.eventEmitter.emitRow(`row:update`, appId, row, table, oldRow) ctx.eventEmitter.emitRow({
eventName: `row:update`,
appId,
row,
table,
oldRow,
user: sdk.users.getUserContextBindings(ctx.user),
})
ctx.message = `${table.name} updated successfully.` ctx.message = `${table.name} updated successfully.`
ctx.body = row ctx.body = row
gridSocket?.emitRowUpdate(ctx, row) gridSocket?.emitRowUpdate(ctx, row)
@ -96,7 +103,14 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
sdk.rows.save(sourceId, ctx.request.body, ctx.user?._id) sdk.rows.save(sourceId, ctx.request.body, ctx.user?._id)
) )
ctx.status = 200 ctx.status = 200
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table) ctx.eventEmitter &&
ctx.eventEmitter.emitRow({
eventName: `row:save`,
appId,
row,
table,
user: sdk.users.getUserContextBindings(ctx.user),
})
ctx.message = `${table.name} saved successfully` ctx.message = `${table.name} saved successfully`
// prefer squashed for response // prefer squashed for response
ctx.body = row || squashed ctx.body = row || squashed
@ -168,10 +182,15 @@ async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
} }
for (let row of rows) { for (let row of rows) {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) ctx.eventEmitter &&
ctx.eventEmitter.emitRow({
eventName: `row:delete`,
appId,
row,
user: sdk.users.getUserContextBindings(ctx.user),
})
gridSocket?.emitRowDeletion(ctx, row) gridSocket?.emitRowDeletion(ctx, row)
} }
return rows return rows
} }
@ -184,7 +203,13 @@ async function deleteRow(ctx: UserCtx<DeleteRowRequest>) {
await quotas.removeRow() await quotas.removeRow()
} }
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, resp.row) ctx.eventEmitter &&
ctx.eventEmitter.emitRow({
eventName: `row:delete`,
appId,
row: resp.row,
user: sdk.users.getUserContextBindings(ctx.user),
})
gridSocket?.emitRowDeletion(ctx, resp.row) gridSocket?.emitRowDeletion(ctx, resp.row)
return resp return resp

View File

@ -5,6 +5,6 @@ export async function run(ctx: Ctx<RowActionTriggerRequest, void>) {
const { tableId, actionId } = ctx.params const { tableId, actionId } = ctx.params
const { rowId } = ctx.request.body const { rowId } = ctx.request.body
await sdk.rowActions.run(tableId, actionId, rowId) await sdk.rowActions.run(tableId, actionId, rowId, ctx.user)
ctx.status = 200 ctx.status = 200
} }

View File

@ -2,11 +2,12 @@ import { InvalidFileExtensions } from "@budibase/shared-core"
import AppComponent from "./templates/BudibaseApp.svelte" import AppComponent from "./templates/BudibaseApp.svelte"
import { join } from "../../../utilities/centralPath" import { join } from "../../../utilities/centralPath"
import * as uuid from "uuid" import * as uuid from "uuid"
import { devClientVersion, ObjectStoreBuckets } from "../../../constants" import { ObjectStoreBuckets } from "../../../constants"
import { processString } from "@budibase/string-templates" import { processString } from "@budibase/string-templates"
import { import {
loadHandlebarsFile, loadHandlebarsFile,
NODE_MODULES_PATH, NODE_MODULES_PATH,
shouldServeLocally,
TOP_LEVEL_PATH, TOP_LEVEL_PATH,
} from "../../../utilities/fileSystem" } from "../../../utilities/fileSystem"
import env from "../../../environment" import env from "../../../environment"
@ -257,25 +258,29 @@ export const serveBuilderPreview = async function (ctx: Ctx) {
export const serveClientLibrary = async function (ctx: Ctx) { export const serveClientLibrary = async function (ctx: Ctx) {
const version = ctx.request.query.version const version = ctx.request.query.version
if (Array.isArray(version)) {
ctx.throw(400)
}
const appId = context.getAppId() || (ctx.request.query.appId as string) const appId = context.getAppId() || (ctx.request.query.appId as string)
let rootPath = join(NODE_MODULES_PATH, "@budibase", "client", "dist") let rootPath = join(NODE_MODULES_PATH, "@budibase", "client", "dist")
if (!appId) { if (!appId) {
ctx.throw(400, "No app ID provided - cannot fetch client library.") ctx.throw(400, "No app ID provided - cannot fetch client library.")
} }
if (env.isProd() || (env.isDev() && version !== devClientVersion)) {
const serveLocally = shouldServeLocally(version || "")
if (!serveLocally) {
ctx.body = await objectStore.getReadStream( ctx.body = await objectStore.getReadStream(
ObjectStoreBuckets.APPS, ObjectStoreBuckets.APPS,
objectStore.clientLibraryPath(appId!) objectStore.clientLibraryPath(appId!)
) )
ctx.set("Content-Type", "application/javascript") ctx.set("Content-Type", "application/javascript")
} else if (env.isDev() && version === devClientVersion) { } else {
// incase running from TS directly // incase running from TS directly
const tsPath = join(require.resolve("@budibase/client"), "..") const tsPath = join(require.resolve("@budibase/client"), "..")
return send(ctx, "budibase-client.js", { return send(ctx, "budibase-client.js", {
root: !fs.existsSync(rootPath) ? tsPath : rootPath, root: !fs.existsSync(rootPath) ? tsPath : rootPath,
}) })
} else {
ctx.throw(500, "Unable to retrieve client library.")
} }
} }

View File

@ -767,7 +767,6 @@ describe("/rowsActions", () => {
it("can trigger an automation given valid data", async () => { it("can trigger an automation given valid data", async () => {
expect(await getAutomationLogs()).toBeEmpty() expect(await getAutomationLogs()).toBeEmpty()
await config.api.rowAction.trigger(viewId, rowAction.id, { rowId }) await config.api.rowAction.trigger(viewId, rowAction.id, { rowId })
const automationLogs = await getAutomationLogs() const automationLogs = await getAutomationLogs()
expect(automationLogs).toEqual([ expect(automationLogs).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -783,6 +782,10 @@ describe("/rowsActions", () => {
...(await config.api.table.get(tableId)), ...(await config.api.table.get(tableId)),
views: expect.anything(), views: expect.anything(),
}, },
user: expect.objectContaining({
_id: "ro_ta_users_" + config.getUser()._id,
}),
automation: expect.objectContaining({ automation: expect.objectContaining({
_id: rowAction.automationId, _id: rowAction.automationId,
}), }),

View File

@ -482,4 +482,38 @@ describe("Automation Scenarios", () => {
} }
) )
}) })
it("Check user is passed through from row trigger", async () => {
const table = await config.createTable()
const builder = createAutomationBuilder({
name: "Test a user is successfully passed from the trigger",
})
const results = await builder
.rowUpdated(
{ tableId: table._id! },
{
row: { name: "Test", description: "TEST" },
id: "1234",
}
)
.serverLog({ text: "{{ [user].[email] }}" })
.run()
expect(results.steps[0].outputs.message).toContain("example.com")
})
it("Check user is passed through from app trigger", async () => {
const builder = createAutomationBuilder({
name: "Test a user is successfully passed from the trigger",
})
const results = await builder
.appAction({ fields: {} })
.serverLog({ text: "{{ [user].[email] }}" })
.run()
expect(results.steps[0].outputs.message).toContain("example.com")
})
}) })

View File

@ -19,6 +19,7 @@ import {
AutomationStoppedReason, AutomationStoppedReason,
AutomationStatus, AutomationStatus,
AutomationRowEvent, AutomationRowEvent,
UserBindings,
} from "@budibase/types" } from "@budibase/types"
import { executeInThread } from "../threads/automation" import { executeInThread } from "../threads/automation"
import { dataFilters, sdk } from "@budibase/shared-core" import { dataFilters, sdk } from "@budibase/shared-core"
@ -140,7 +141,12 @@ function rowPassesFilters(row: Row, filters: SearchFilters) {
export async function externalTrigger( export async function externalTrigger(
automation: Automation, automation: Automation,
params: { fields: Record<string, any>; timeout?: number; appId?: string }, params: {
fields: Record<string, any>
timeout?: number
appId?: string
user?: UserBindings
},
{ getResponses }: { getResponses?: boolean } = {} { getResponses }: { getResponses?: boolean } = {}
): Promise<any> { ): Promise<any> {
if (automation.disabled) { if (automation.disabled) {

View File

@ -152,8 +152,6 @@ export enum AutomationErrors {
FAILURE_CONDITION = "FAILURE_CONDITION_MET", FAILURE_CONDITION = "FAILURE_CONDITION_MET",
} }
export const devClientVersion = "0.0.0"
// pass through the list from the auth/core lib // pass through the list from the auth/core lib
export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets
export const MAX_AUTOMATION_RECURRING_ERRORS = 5 export const MAX_AUTOMATION_RECURRING_ERRORS = 5

View File

@ -1,4 +1,4 @@
import { AutomationResults, LoopStepType } from "@budibase/types" import { AutomationResults, LoopStepType, UserBindings } from "@budibase/types"
export interface LoopInput { export interface LoopInput {
option: LoopStepType option: LoopStepType
@ -18,5 +18,6 @@ export interface AutomationContext extends AutomationResults {
stepsById: Record<string, any> stepsById: Record<string, any>
stepsByName: Record<string, any> stepsByName: Record<string, any>
env?: Record<string, string> env?: Record<string, string>
user?: UserBindings
trigger: any trigger: any
} }

View File

@ -31,7 +31,17 @@ class AutomationEmitter {
} }
} }
async emitRow(eventName: string, appId: string, row: Row, table?: Table) { async emitRow({
eventName,
appId,
row,
table,
}: {
eventName: string
appId: string
row: Row
table?: Table
}) {
let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain() let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain()
// don't emit even if we've reached max automation chain // don't emit even if we've reached max automation chain

View File

@ -1,6 +1,6 @@
import { EventEmitter } from "events" import { EventEmitter } from "events"
import { rowEmission, tableEmission } from "./utils" import { rowEmission, tableEmission } from "./utils"
import { Table, Row } from "@budibase/types" import { Table, Row, User } from "@budibase/types"
/** /**
* keeping event emitter in one central location as it might be used for things other than * keeping event emitter in one central location as it might be used for things other than
@ -13,14 +13,22 @@ import { Table, Row } from "@budibase/types"
* This is specifically quite important for template strings used in automations. * This is specifically quite important for template strings used in automations.
*/ */
class BudibaseEmitter extends EventEmitter { class BudibaseEmitter extends EventEmitter {
emitRow( emitRow({
eventName: string, eventName,
appId: string, appId,
row: Row, row,
table?: Table, table,
oldRow,
user,
}: {
eventName: string
appId: string
row: Row
table?: Table
oldRow?: Row oldRow?: Row
) { user: User
rowEmission({ emitter: this, eventName, appId, row, table, oldRow }) }) {
rowEmission({ emitter: this, eventName, appId, row, table, oldRow, user })
} }
emitTable(eventName: string, appId: string, table?: Table) { emitTable(eventName: string, appId: string, table?: Table) {

View File

@ -1,4 +1,4 @@
import { Table, Row } from "@budibase/types" import { Table, Row, User } from "@budibase/types"
import BudibaseEmitter from "./BudibaseEmitter" import BudibaseEmitter from "./BudibaseEmitter"
type BBEventOpts = { type BBEventOpts = {
@ -9,6 +9,7 @@ type BBEventOpts = {
row?: Row row?: Row
oldRow?: Row oldRow?: Row
metadata?: any metadata?: any
user?: User
} }
interface BBEventTable extends Table { interface BBEventTable extends Table {
@ -24,6 +25,7 @@ type BBEvent = {
id?: string id?: string
revision?: string revision?: string
metadata?: any metadata?: any
user?: User
} }
export function rowEmission({ export function rowEmission({
@ -34,12 +36,14 @@ export function rowEmission({
table, table,
metadata, metadata,
oldRow, oldRow,
user,
}: BBEventOpts) { }: BBEventOpts) {
let event: BBEvent = { let event: BBEvent = {
row, row,
oldRow, oldRow,
appId, appId,
tableId: row?.tableId, tableId: row?.tableId,
user,
} }
if (table) { if (table) {
event.table = table event.table = table

View File

@ -4,6 +4,7 @@ import {
AutomationTriggerStepId, AutomationTriggerStepId,
SEPARATOR, SEPARATOR,
TableRowActions, TableRowActions,
User,
VirtualDocumentType, VirtualDocumentType,
} from "@budibase/types" } from "@budibase/types"
import { generateRowActionsID } from "../../db/utils" import { generateRowActionsID } from "../../db/utils"
@ -236,7 +237,12 @@ export async function remove(tableId: string, rowActionId: string) {
}) })
} }
export async function run(tableId: any, rowActionId: any, rowId: string) { export async function run(
tableId: any,
rowActionId: any,
rowId: string,
user: User
) {
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
if (!table) { if (!table) {
throw new HTTPError("Table not found", 404) throw new HTTPError("Table not found", 404)
@ -258,6 +264,7 @@ export async function run(tableId: any, rowActionId: any, rowId: string) {
row, row,
table, table,
}, },
user,
appId: context.getAppId(), appId: context.getAppId(),
}, },
{ getResponses: true } { getResponses: true }

View File

@ -12,6 +12,7 @@ import {
UserMetadata, UserMetadata,
Database, Database,
ContextUserMetadata, ContextUserMetadata,
UserBindings,
} from "@budibase/types" } from "@budibase/types"
export function combineMetadataAndUser( export function combineMetadataAndUser(
@ -125,7 +126,7 @@ export async function syncGlobalUsers() {
} }
} }
export function getUserContextBindings(user: ContextUser) { export function getUserContextBindings(user: ContextUser): UserBindings {
if (!user) { if (!user) {
return {} return {}
} }

View File

@ -26,6 +26,7 @@ import {
BranchStep, BranchStep,
LoopStep, LoopStep,
SearchFilters, SearchFilters,
UserBindings,
} from "@budibase/types" } from "@budibase/types"
import { AutomationContext, TriggerOutput } from "../definitions/automations" import { AutomationContext, TriggerOutput } from "../definitions/automations"
import { WorkerCallback } from "./definitions" import { WorkerCallback } from "./definitions"
@ -75,6 +76,7 @@ class Orchestrator {
private loopStepOutputs: LoopStep[] private loopStepOutputs: LoopStep[]
private stopped: boolean private stopped: boolean
private executionOutput: Omit<AutomationContext, "stepsByName" | "stepsById"> private executionOutput: Omit<AutomationContext, "stepsByName" | "stepsById">
private currentUser: UserBindings | undefined
constructor(job: AutomationJob) { constructor(job: AutomationJob) {
let automation = job.data.automation let automation = job.data.automation
@ -106,6 +108,7 @@ class Orchestrator {
this.updateExecutionOutput(triggerId, triggerStepId, null, triggerOutput) this.updateExecutionOutput(triggerId, triggerStepId, null, triggerOutput)
this.loopStepOutputs = [] this.loopStepOutputs = []
this.stopped = false this.stopped = false
this.currentUser = triggerOutput.user
} }
cleanupTriggerOutputs(stepId: string, triggerOutput: TriggerOutput) { cleanupTriggerOutputs(stepId: string, triggerOutput: TriggerOutput) {
@ -258,6 +261,7 @@ class Orchestrator {
automationId: this.automation._id, automationId: this.automation._id,
}) })
this.context.env = await sdkUtils.getEnvironmentVariables() this.context.env = await sdkUtils.getEnvironmentVariables()
this.context.user = this.currentUser
let metadata let metadata
@ -572,7 +576,6 @@ class Orchestrator {
originalStepInput, originalStepInput,
this.processContext(this.context) this.processContext(this.context)
) )
inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs) inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs)
const outputs = await stepFn({ const outputs = await stepFn({

View File

@ -1,8 +1,8 @@
import { budibaseTempDir } from "../budibaseDir" import { budibaseTempDir } from "../budibaseDir"
import fs from "fs" import fs from "fs"
import { join } from "path" import { join } from "path"
import { ObjectStoreBuckets, devClientVersion } from "../../constants" import { ObjectStoreBuckets } from "../../constants"
import { updateClientLibrary } from "./clientLibrary" import { shouldServeLocally, updateClientLibrary } from "./clientLibrary"
import env from "../../environment" import env from "../../environment"
import { objectStore, context } from "@budibase/backend-core" import { objectStore, context } from "@budibase/backend-core"
import { TOP_LEVEL_PATH } from "./filesystem" import { TOP_LEVEL_PATH } from "./filesystem"
@ -40,7 +40,7 @@ export const getComponentLibraryManifest = async (library: string) => {
const db = context.getAppDB() const db = context.getAppDB()
const app = await db.get<App>(DocumentType.APP_METADATA) const app = await db.get<App>(DocumentType.APP_METADATA)
if (app.version === devClientVersion || env.isTest()) { if (shouldServeLocally(app.version) || env.isTest()) {
const paths = [ const paths = [
join(TOP_LEVEL_PATH, "packages/client", filename), join(TOP_LEVEL_PATH, "packages/client", filename),
join(process.cwd(), "client", filename), join(process.cwd(), "client", filename),

View File

@ -1,3 +1,4 @@
import semver from "semver"
import path, { join } from "path" import path, { join } from "path"
import { ObjectStoreBuckets } from "../../constants" import { ObjectStoreBuckets } from "../../constants"
import fs from "fs" import fs from "fs"
@ -183,3 +184,19 @@ export async function revertClientLibrary(appId: string) {
return JSON.parse(await manifestSrc) return JSON.parse(await manifestSrc)
} }
export function shouldServeLocally(version: string) {
if (env.isProd() || !env.isDev()) {
return false
}
if (version === "0.0.0") {
return true
}
const parsedSemver = semver.parse(version)
if (parsedSemver?.build?.[0] === "local") {
return true
}
return false
}

View File

@ -261,6 +261,7 @@ export type UpdatedRowEventEmitter = {
oldRow: Row oldRow: Row
table: Table table: Table
appId: string appId: string
user: User
} }
export enum LoopStepType { export enum LoopStepType {

View File

@ -68,6 +68,16 @@ export interface User extends Document {
appSort?: string appSort?: string
} }
export interface UserBindings extends Document {
firstName?: string
lastName?: string
email?: string
status?: string
roleId?: string | null
globalId?: string
userId?: string
}
export enum UserStatus { export enum UserStatus {
ACTIVE = "active", ACTIVE = "active",
INACTIVE = "inactive", INACTIVE = "inactive",

View File

@ -1,4 +1,9 @@
import { Automation, AutomationMetadata, Row } from "../../documents" import {
Automation,
AutomationMetadata,
Row,
UserBindings,
} from "../../documents"
import { Job } from "bull" import { Job } from "bull"
export interface AutomationDataEvent { export interface AutomationDataEvent {
@ -8,6 +13,7 @@ export interface AutomationDataEvent {
timeout?: number timeout?: number
row?: Row row?: Row
oldRow?: Row oldRow?: Row
user?: UserBindings
} }
export interface AutomationData { export interface AutomationData {