@budibase/backend-core 3.2.0 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2181 -1429
- package/dist/index.js.map +4 -4
- package/dist/index.js.meta.json +1 -1
- package/dist/package.json +4 -4
- package/dist/plugins.js.meta.json +1 -1
- package/dist/src/environment.d.ts +2 -0
- package/dist/src/environment.js +3 -1
- package/dist/src/environment.js.map +1 -1
- package/dist/src/middleware/contentSecurityPolicy.d.ts +2 -0
- package/dist/src/middleware/contentSecurityPolicy.js +128 -0
- package/dist/src/middleware/contentSecurityPolicy.js.map +1 -0
- package/dist/src/middleware/index.d.ts +1 -0
- package/dist/src/middleware/index.js +3 -1
- package/dist/src/middleware/index.js.map +1 -1
- package/dist/src/queue/inMemoryQueue.d.ts +0 -2
- package/dist/src/queue/inMemoryQueue.js +1 -11
- package/dist/src/queue/inMemoryQueue.js.map +1 -1
- package/dist/src/queue/queue.js +6 -0
- package/dist/src/queue/queue.js.map +1 -1
- package/dist/tests/core/utilities/index.d.ts +1 -0
- package/dist/tests/core/utilities/index.js +2 -1
- package/dist/tests/core/utilities/index.js.map +1 -1
- package/dist/tests/core/utilities/queue.d.ts +2 -0
- package/dist/tests/core/utilities/queue.js +21 -0
- package/dist/tests/core/utilities/queue.js.map +1 -0
- package/dist/tests/core/utilities/testContainerUtils.d.ts +2 -0
- package/dist/tests/core/utilities/testContainerUtils.js +63 -0
- package/dist/tests/core/utilities/testContainerUtils.js.map +1 -1
- package/dist/tests/core/utilities/utils/index.d.ts +1 -0
- package/dist/tests/core/utilities/utils/index.js +2 -1
- package/dist/tests/core/utilities/utils/index.js.map +1 -1
- package/dist/tests/core/utilities/utils/queue.d.ts +3 -0
- package/dist/tests/core/utilities/utils/queue.js +37 -0
- package/dist/tests/core/utilities/utils/queue.js.map +1 -0
- package/package.json +4 -4
- package/src/cache/tests/docWritethrough.spec.ts +26 -24
- package/src/environment.ts +4 -0
- package/src/middleware/contentSecurityPolicy.ts +118 -0
- package/src/middleware/index.ts +1 -0
- package/src/middleware/tests/contentSecurityPolicy.spec.ts +75 -0
- package/src/queue/inMemoryQueue.ts +2 -12
- package/src/queue/queue.ts +10 -3
- package/tests/core/utilities/index.ts +1 -0
- package/tests/core/utilities/queue.ts +9 -0
- package/tests/core/utilities/testContainerUtils.ts +57 -0
- package/tests/core/utilities/utils/index.ts +1 -0
- package/tests/core/utilities/utils/queue.ts +27 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
import env from "../environment"
|
|
3
|
+
|
|
4
|
+
const CSP_DIRECTIVES = {
|
|
5
|
+
"default-src": ["'self'"],
|
|
6
|
+
"script-src": [
|
|
7
|
+
"'self'",
|
|
8
|
+
"'unsafe-eval'",
|
|
9
|
+
"https://*.budibase.net",
|
|
10
|
+
"https://cdn.budi.live",
|
|
11
|
+
"https://js.intercomcdn.com",
|
|
12
|
+
"https://widget.intercom.io",
|
|
13
|
+
"https://d2l5prqdbvm3op.cloudfront.net",
|
|
14
|
+
"https://us-assets.i.posthog.com",
|
|
15
|
+
],
|
|
16
|
+
"style-src": [
|
|
17
|
+
"'self'",
|
|
18
|
+
"'unsafe-inline'",
|
|
19
|
+
"https://cdn.jsdelivr.net",
|
|
20
|
+
"https://fonts.googleapis.com",
|
|
21
|
+
"https://rsms.me",
|
|
22
|
+
"https://maxcdn.bootstrapcdn.com",
|
|
23
|
+
],
|
|
24
|
+
"object-src": ["'none'"],
|
|
25
|
+
"base-uri": ["'self'"],
|
|
26
|
+
"connect-src": [
|
|
27
|
+
"'self'",
|
|
28
|
+
"https://*.budibase.app",
|
|
29
|
+
"https://*.budibaseqa.app",
|
|
30
|
+
"https://*.budibase.net",
|
|
31
|
+
"https://api-iam.intercom.io",
|
|
32
|
+
"https://api-ping.intercom.io",
|
|
33
|
+
"https://app.posthog.com",
|
|
34
|
+
"https://us.i.posthog.com",
|
|
35
|
+
"wss://nexus-websocket-a.intercom.io",
|
|
36
|
+
"wss://nexus-websocket-b.intercom.io",
|
|
37
|
+
"https://nexus-websocket-a.intercom.io",
|
|
38
|
+
"https://nexus-websocket-b.intercom.io",
|
|
39
|
+
"https://uploads.intercomcdn.com",
|
|
40
|
+
"https://uploads.intercomusercontent.com",
|
|
41
|
+
"https://*.amazonaws.com",
|
|
42
|
+
"https://*.s3.amazonaws.com",
|
|
43
|
+
"https://*.s3.us-east-2.amazonaws.com",
|
|
44
|
+
"https://*.s3.us-east-1.amazonaws.com",
|
|
45
|
+
"https://*.s3.us-west-1.amazonaws.com",
|
|
46
|
+
"https://*.s3.us-west-2.amazonaws.com",
|
|
47
|
+
"https://*.s3.af-south-1.amazonaws.com",
|
|
48
|
+
"https://*.s3.ap-east-1.amazonaws.com",
|
|
49
|
+
"https://*.s3.ap-south-1.amazonaws.com",
|
|
50
|
+
"https://*.s3.ap-northeast-2.amazonaws.com",
|
|
51
|
+
"https://*.s3.ap-southeast-1.amazonaws.com",
|
|
52
|
+
"https://*.s3.ap-southeast-2.amazonaws.com",
|
|
53
|
+
"https://*.s3.ap-northeast-1.amazonaws.com",
|
|
54
|
+
"https://*.s3.ca-central-1.amazonaws.com",
|
|
55
|
+
"https://*.s3.cn-north-1.amazonaws.com",
|
|
56
|
+
"https://*.s3.cn-northwest-1.amazonaws.com",
|
|
57
|
+
"https://*.s3.eu-central-1.amazonaws.com",
|
|
58
|
+
"https://*.s3.eu-west-1.amazonaws.com",
|
|
59
|
+
"https://*.s3.eu-west-2.amazonaws.com",
|
|
60
|
+
"https://*.s3.eu-south-1.amazonaws.com",
|
|
61
|
+
"https://*.s3.eu-west-3.amazonaws.com",
|
|
62
|
+
"https://*.s3.eu-north-1.amazonaws.com",
|
|
63
|
+
"https://*.s3.sa-east-1.amazonaws.com",
|
|
64
|
+
"https://*.s3.me-south-1.amazonaws.com",
|
|
65
|
+
"https://*.s3.us-gov-east-1.amazonaws.com",
|
|
66
|
+
"https://*.s3.us-gov-west-1.amazonaws.com",
|
|
67
|
+
"https://api.github.com",
|
|
68
|
+
],
|
|
69
|
+
"font-src": [
|
|
70
|
+
"'self'",
|
|
71
|
+
"data:",
|
|
72
|
+
"https://cdn.jsdelivr.net",
|
|
73
|
+
"https://fonts.gstatic.com",
|
|
74
|
+
"https://rsms.me",
|
|
75
|
+
"https://maxcdn.bootstrapcdn.com",
|
|
76
|
+
"https://js.intercomcdn.com",
|
|
77
|
+
"https://fonts.intercomcdn.com",
|
|
78
|
+
],
|
|
79
|
+
"frame-src": ["'self'", "https:"],
|
|
80
|
+
"img-src": ["http:", "https:", "data:", "blob:"],
|
|
81
|
+
"manifest-src": ["'self'"],
|
|
82
|
+
"media-src": [
|
|
83
|
+
"'self'",
|
|
84
|
+
"https://js.intercomcdn.com",
|
|
85
|
+
"https://cdn.budi.live",
|
|
86
|
+
],
|
|
87
|
+
"worker-src": ["blob:"],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function contentSecurityPolicy(ctx: any, next: any) {
|
|
91
|
+
try {
|
|
92
|
+
const nonce = crypto.randomBytes(16).toString("base64")
|
|
93
|
+
|
|
94
|
+
const directives = { ...CSP_DIRECTIVES }
|
|
95
|
+
directives["script-src"] = [
|
|
96
|
+
...CSP_DIRECTIVES["script-src"],
|
|
97
|
+
`'nonce-${nonce}'`,
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
if (!env.DISABLE_CSP_UNSAFE_INLINE_SCRIPTS) {
|
|
101
|
+
directives["script-src"].push("'unsafe-inline'")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
ctx.state.nonce = nonce
|
|
105
|
+
|
|
106
|
+
const cspHeader = Object.entries(directives)
|
|
107
|
+
.map(([key, sources]) => `${key} ${sources.join(" ")}`)
|
|
108
|
+
.join("; ")
|
|
109
|
+
ctx.set("Content-Security-Policy", cspHeader)
|
|
110
|
+
await next()
|
|
111
|
+
} catch (err: any) {
|
|
112
|
+
console.error(
|
|
113
|
+
`Error occurred in Content-Security-Policy middleware: ${err}`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default contentSecurityPolicy
|
package/src/middleware/index.ts
CHANGED
|
@@ -19,5 +19,6 @@ export { default as pino } from "../logging/pino/middleware"
|
|
|
19
19
|
export { default as correlation } from "../logging/correlation/middleware"
|
|
20
20
|
export { default as errorHandling } from "./errorHandling"
|
|
21
21
|
export { default as querystringToBody } from "./querystringToBody"
|
|
22
|
+
export { default as csp } from "./contentSecurityPolicy"
|
|
22
23
|
export * as joiValidator from "./joi-validator"
|
|
23
24
|
export { default as ip } from "./ip"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
import contentSecurityPolicy from "../contentSecurityPolicy"
|
|
3
|
+
|
|
4
|
+
jest.mock("crypto", () => ({
|
|
5
|
+
randomBytes: jest.fn(),
|
|
6
|
+
randomUUID: jest.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
describe("contentSecurityPolicy middleware", () => {
|
|
10
|
+
let ctx: any
|
|
11
|
+
let next: any
|
|
12
|
+
const mockNonce = "mocked/nonce"
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
ctx = {
|
|
16
|
+
state: {},
|
|
17
|
+
set: jest.fn(),
|
|
18
|
+
}
|
|
19
|
+
next = jest.fn()
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
crypto.randomBytes.mockReturnValue(Buffer.from(mockNonce, "base64"))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
jest.clearAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("should generate a nonce and set it in the script-src directive", async () => {
|
|
29
|
+
await contentSecurityPolicy(ctx, next)
|
|
30
|
+
|
|
31
|
+
expect(ctx.state.nonce).toBe(mockNonce)
|
|
32
|
+
expect(ctx.set).toHaveBeenCalledWith(
|
|
33
|
+
"Content-Security-Policy",
|
|
34
|
+
expect.stringContaining(
|
|
35
|
+
`script-src 'self' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io https://d2l5prqdbvm3op.cloudfront.net https://us-assets.i.posthog.com 'nonce-${mockNonce}'`
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
expect(next).toHaveBeenCalled()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should include all CSP directives in the header", async () => {
|
|
42
|
+
await contentSecurityPolicy(ctx, next)
|
|
43
|
+
|
|
44
|
+
const cspHeader = ctx.set.mock.calls[0][1]
|
|
45
|
+
expect(cspHeader).toContain("default-src 'self'")
|
|
46
|
+
expect(cspHeader).toContain("script-src 'self' 'unsafe-eval'")
|
|
47
|
+
expect(cspHeader).toContain("style-src 'self' 'unsafe-inline'")
|
|
48
|
+
expect(cspHeader).toContain("object-src 'none'")
|
|
49
|
+
expect(cspHeader).toContain("base-uri 'self'")
|
|
50
|
+
expect(cspHeader).toContain("connect-src 'self'")
|
|
51
|
+
expect(cspHeader).toContain("font-src 'self'")
|
|
52
|
+
expect(cspHeader).toContain("frame-src 'self'")
|
|
53
|
+
expect(cspHeader).toContain("img-src http: https: data: blob:")
|
|
54
|
+
expect(cspHeader).toContain("manifest-src 'self'")
|
|
55
|
+
expect(cspHeader).toContain("media-src 'self'")
|
|
56
|
+
expect(cspHeader).toContain("worker-src blob:")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should handle errors and log an error message", async () => {
|
|
60
|
+
const consoleSpy = jest.spyOn(console, "error").mockImplementation()
|
|
61
|
+
const error = new Error("Test error")
|
|
62
|
+
// @ts-ignore
|
|
63
|
+
crypto.randomBytes.mockImplementation(() => {
|
|
64
|
+
throw error
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await contentSecurityPolicy(ctx, next)
|
|
68
|
+
|
|
69
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
70
|
+
`Error occurred in Content-Security-Policy middleware: ${error}`
|
|
71
|
+
)
|
|
72
|
+
expect(next).not.toHaveBeenCalled()
|
|
73
|
+
consoleSpy.mockRestore()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import events from "events"
|
|
2
|
-
import { newid
|
|
2
|
+
import { newid } from "../utils"
|
|
3
3
|
import { Queue, QueueOptions, JobOptions } from "./queue"
|
|
4
4
|
|
|
5
5
|
interface JobMessage {
|
|
@@ -141,7 +141,7 @@ class InMemoryQueue implements Partial<Queue> {
|
|
|
141
141
|
} else {
|
|
142
142
|
pushMessage()
|
|
143
143
|
}
|
|
144
|
-
return {} as any
|
|
144
|
+
return { id: jobId } as any
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/**
|
|
@@ -184,16 +184,6 @@ class InMemoryQueue implements Partial<Queue> {
|
|
|
184
184
|
// do nothing
|
|
185
185
|
return this as any
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
async waitForCompletion() {
|
|
189
|
-
do {
|
|
190
|
-
await timeout(50)
|
|
191
|
-
} while (this.hasRunningJobs())
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
hasRunningJobs() {
|
|
195
|
-
return this._addCount > this._runCount
|
|
196
|
-
}
|
|
197
187
|
}
|
|
198
188
|
|
|
199
189
|
export default InMemoryQueue
|
package/src/queue/queue.ts
CHANGED
|
@@ -15,7 +15,7 @@ const QUEUE_LOCK_MS = Duration.fromMinutes(5).toMs()
|
|
|
15
15
|
const QUEUE_LOCK_RENEW_INTERNAL_MS = Duration.fromSeconds(30).toMs()
|
|
16
16
|
// cleanup the queue every 60 seconds
|
|
17
17
|
const CLEANUP_PERIOD_MS = Duration.fromSeconds(60).toMs()
|
|
18
|
-
let QUEUES: BullQueue.Queue[]
|
|
18
|
+
let QUEUES: BullQueue.Queue[] = []
|
|
19
19
|
let cleanupInterval: NodeJS.Timeout
|
|
20
20
|
|
|
21
21
|
async function cleanup() {
|
|
@@ -45,11 +45,18 @@ export function createQueue<T>(
|
|
|
45
45
|
if (opts.jobOptions) {
|
|
46
46
|
queueConfig.defaultJobOptions = opts.jobOptions
|
|
47
47
|
}
|
|
48
|
-
let queue:
|
|
48
|
+
let queue: BullQueue.Queue<T>
|
|
49
49
|
if (!env.isTest()) {
|
|
50
50
|
queue = new BullQueue(jobQueue, queueConfig)
|
|
51
|
+
} else if (
|
|
52
|
+
process.env.BULL_TEST_REDIS_PORT &&
|
|
53
|
+
!isNaN(+process.env.BULL_TEST_REDIS_PORT)
|
|
54
|
+
) {
|
|
55
|
+
queue = new BullQueue(jobQueue, {
|
|
56
|
+
redis: { host: "localhost", port: +process.env.BULL_TEST_REDIS_PORT },
|
|
57
|
+
})
|
|
51
58
|
} else {
|
|
52
|
-
queue = new InMemoryQueue(jobQueue, queueConfig)
|
|
59
|
+
queue = new InMemoryQueue(jobQueue, queueConfig) as any
|
|
53
60
|
}
|
|
54
61
|
addListeners(queue, jobQueue, opts?.removeStalledCb)
|
|
55
62
|
QUEUES.push(queue)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execSync } from "child_process"
|
|
2
|
+
import { cloneDeep } from "lodash"
|
|
3
|
+
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
|
2
4
|
|
|
3
5
|
const IPV4_PORT_REGEX = new RegExp(`0\\.0\\.0\\.0:(\\d+)->(\\d+)/tcp`, "g")
|
|
4
6
|
|
|
@@ -106,3 +108,58 @@ export function setupEnv(...envs: any[]) {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
}
|
|
111
|
+
|
|
112
|
+
export async function startContainer(container: GenericContainer) {
|
|
113
|
+
const imageName = (container as any).imageName.string as string
|
|
114
|
+
let key: string = imageName
|
|
115
|
+
if (imageName.includes("@sha256")) {
|
|
116
|
+
key = imageName.split("@")[0]
|
|
117
|
+
}
|
|
118
|
+
key = key.replace(/\//g, "-").replace(/:/g, "-")
|
|
119
|
+
|
|
120
|
+
container = container
|
|
121
|
+
.withReuse()
|
|
122
|
+
.withLabels({ "com.budibase": "true" })
|
|
123
|
+
.withName(`${key}_testcontainer`)
|
|
124
|
+
|
|
125
|
+
let startedContainer: StartedTestContainer | undefined = undefined
|
|
126
|
+
let lastError = undefined
|
|
127
|
+
for (let i = 0; i < 10; i++) {
|
|
128
|
+
try {
|
|
129
|
+
// container.start() is not an idempotent operation, calling `start`
|
|
130
|
+
// modifies the internal state of a GenericContainer instance such that
|
|
131
|
+
// the hash it uses to determine reuse changes. We need to clone the
|
|
132
|
+
// container before calling start to ensure that we're using the same
|
|
133
|
+
// reuse hash every time.
|
|
134
|
+
const containerCopy = cloneDeep(container)
|
|
135
|
+
startedContainer = await containerCopy.start()
|
|
136
|
+
lastError = undefined
|
|
137
|
+
break
|
|
138
|
+
} catch (e: any) {
|
|
139
|
+
lastError = e
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!startedContainer) {
|
|
145
|
+
if (lastError) {
|
|
146
|
+
throw lastError
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`failed to start container: ${imageName}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const info = getContainerById(startedContainer.getId())
|
|
152
|
+
if (!info) {
|
|
153
|
+
throw new Error("Container not found")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Some Docker runtimes, when you expose a port, will bind it to both
|
|
157
|
+
// 127.0.0.1 and ::1, so ipv4 and ipv6. The port spaces of ipv4 and ipv6
|
|
158
|
+
// addresses are not shared, and testcontainers will sometimes give you back
|
|
159
|
+
// the ipv6 port. There's no way to know that this has happened, and if you
|
|
160
|
+
// try to then connect to `localhost:port` you may attempt to bind to the v4
|
|
161
|
+
// address which could be unbound or even an entirely different container. For
|
|
162
|
+
// that reason, we don't use testcontainers' `getExposedPort` function,
|
|
163
|
+
// preferring instead our own method that guaranteed v4 ports.
|
|
164
|
+
return getExposedV4Ports(info)
|
|
165
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Queue } from "bull"
|
|
2
|
+
import { GenericContainer, Wait } from "testcontainers"
|
|
3
|
+
import { startContainer } from "../testContainerUtils"
|
|
4
|
+
|
|
5
|
+
export async function useRealQueues() {
|
|
6
|
+
const ports = await startContainer(
|
|
7
|
+
new GenericContainer("redis")
|
|
8
|
+
.withExposedPorts(6379)
|
|
9
|
+
.withWaitStrategy(
|
|
10
|
+
Wait.forSuccessfulCommand(`redis-cli`).withStartupTimeout(10000)
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const port = ports.find(x => x.container === 6379)?.host
|
|
15
|
+
if (!port) {
|
|
16
|
+
throw new Error("Redis port not found")
|
|
17
|
+
}
|
|
18
|
+
process.env.BULL_TEST_REDIS_PORT = port.toString()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function processMessages(queue: Queue) {
|
|
22
|
+
do {
|
|
23
|
+
await queue.whenCurrentJobsFinished()
|
|
24
|
+
} while (await queue.count())
|
|
25
|
+
|
|
26
|
+
await queue.whenCurrentJobsFinished()
|
|
27
|
+
}
|