@budibase/backend-core 2.24.2 → 2.26.0
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 +294 -218
- 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.map +2 -2
- package/dist/plugins.js.meta.json +1 -1
- package/dist/src/cache/user.d.ts +1 -1
- package/dist/src/cache/user.js.map +1 -1
- package/dist/src/context/mainContext.d.ts +1 -1
- package/dist/src/context/mainContext.js +2 -2
- package/dist/src/context/mainContext.js.map +1 -1
- package/dist/src/db/couch/DatabaseImpl.d.ts +2 -2
- package/dist/src/db/couch/DatabaseImpl.js +17 -6
- package/dist/src/db/couch/DatabaseImpl.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/middleware/authenticated.js +10 -6
- package/dist/src/middleware/authenticated.js.map +1 -1
- package/dist/src/objectStore/objectStore.d.ts +7 -2
- package/dist/src/objectStore/objectStore.js +26 -12
- package/dist/src/objectStore/objectStore.js.map +1 -1
- package/dist/src/objectStore/utils.d.ts +3 -0
- package/dist/src/objectStore/utils.js +80 -2
- package/dist/src/objectStore/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/cache/user.ts +2 -2
- package/src/context/mainContext.ts +2 -2
- package/src/db/couch/DatabaseImpl.ts +41 -9
- package/src/middleware/authenticated.ts +18 -8
- package/src/objectStore/objectStore.ts +37 -13
- package/src/objectStore/utils.ts +54 -2
package/src/cache/user.ts
CHANGED
|
@@ -69,7 +69,7 @@ async function populateUsersFromDB(
|
|
|
69
69
|
export async function getUser(
|
|
70
70
|
userId: string,
|
|
71
71
|
tenantId?: string,
|
|
72
|
-
populateUser?:
|
|
72
|
+
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
|
73
73
|
) {
|
|
74
74
|
if (!populateUser) {
|
|
75
75
|
populateUser = populateFromDB
|
|
@@ -83,7 +83,7 @@ export async function getUser(
|
|
|
83
83
|
}
|
|
84
84
|
const client = await redis.getUserClient()
|
|
85
85
|
// try cache
|
|
86
|
-
let user = await client.get(userId)
|
|
86
|
+
let user: User = await client.get(userId)
|
|
87
87
|
if (!user) {
|
|
88
88
|
user = await populateUser(userId, tenantId)
|
|
89
89
|
await client.store(userId, user, EXPIRY_SECONDS)
|
|
@@ -281,7 +281,7 @@ export function doInScimContext(task: any) {
|
|
|
281
281
|
return newContext(updates, task)
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
export async function ensureSnippetContext() {
|
|
284
|
+
export async function ensureSnippetContext(enabled = !env.isTest()) {
|
|
285
285
|
const ctx = getCurrentContext()
|
|
286
286
|
|
|
287
287
|
// If we've already added snippets to context, continue
|
|
@@ -292,7 +292,7 @@ export async function ensureSnippetContext() {
|
|
|
292
292
|
// Otherwise get snippets for this app and update context
|
|
293
293
|
let snippets: Snippet[] | undefined
|
|
294
294
|
const db = getAppDB()
|
|
295
|
-
if (db &&
|
|
295
|
+
if (db && enabled) {
|
|
296
296
|
const app = await db.get<App>(DocumentType.APP_METADATA)
|
|
297
297
|
snippets = app.snippets
|
|
298
298
|
}
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
AllDocsResponse,
|
|
4
4
|
AnyDocument,
|
|
5
5
|
Database,
|
|
6
|
-
DatabaseOpts,
|
|
7
|
-
DatabaseQueryOpts,
|
|
8
|
-
DatabasePutOpts,
|
|
9
6
|
DatabaseCreateIndexOpts,
|
|
10
7
|
DatabaseDeleteIndexOpts,
|
|
8
|
+
DatabaseOpts,
|
|
9
|
+
DatabasePutOpts,
|
|
10
|
+
DatabaseQueryOpts,
|
|
11
11
|
Document,
|
|
12
12
|
isDocument,
|
|
13
13
|
RowResponse,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { getCouchInfo } from "./connections"
|
|
18
18
|
import { directCouchUrlCall } from "./utils"
|
|
19
19
|
import { getPouchDB } from "./pouchDB"
|
|
20
|
-
import {
|
|
20
|
+
import { ReadStream, WriteStream } from "fs"
|
|
21
21
|
import { newid } from "../../docIds/newid"
|
|
22
22
|
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
|
23
23
|
import { DDInstrumentedDatabase } from "../instrumentation"
|
|
@@ -38,6 +38,39 @@ function buildNano(couchInfo: { url: string; cookie: string }) {
|
|
|
38
38
|
|
|
39
39
|
type DBCall<T> = () => Promise<T>
|
|
40
40
|
|
|
41
|
+
class CouchDBError extends Error {
|
|
42
|
+
status: number
|
|
43
|
+
statusCode: number
|
|
44
|
+
reason: string
|
|
45
|
+
name: string
|
|
46
|
+
errid: string
|
|
47
|
+
error: string
|
|
48
|
+
description: string
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
message: string,
|
|
52
|
+
info: {
|
|
53
|
+
status: number | undefined
|
|
54
|
+
statusCode: number | undefined
|
|
55
|
+
name: string
|
|
56
|
+
errid: string
|
|
57
|
+
description: string
|
|
58
|
+
reason: string
|
|
59
|
+
error: string
|
|
60
|
+
}
|
|
61
|
+
) {
|
|
62
|
+
super(message)
|
|
63
|
+
const statusCode = info.status || info.statusCode || 500
|
|
64
|
+
this.status = statusCode
|
|
65
|
+
this.statusCode = statusCode
|
|
66
|
+
this.reason = info.reason
|
|
67
|
+
this.name = info.name
|
|
68
|
+
this.errid = info.errid
|
|
69
|
+
this.description = info.description
|
|
70
|
+
this.error = info.error
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
41
74
|
export function DatabaseWithConnection(
|
|
42
75
|
dbName: string,
|
|
43
76
|
connection: string,
|
|
@@ -119,7 +152,7 @@ export class DatabaseImpl implements Database {
|
|
|
119
152
|
} catch (err: any) {
|
|
120
153
|
// Handling race conditions
|
|
121
154
|
if (err.statusCode !== 412) {
|
|
122
|
-
throw err
|
|
155
|
+
throw new CouchDBError(err.message, err)
|
|
123
156
|
}
|
|
124
157
|
}
|
|
125
158
|
}
|
|
@@ -138,10 +171,9 @@ export class DatabaseImpl implements Database {
|
|
|
138
171
|
if (err.statusCode === 404 && err.reason === DATABASE_NOT_FOUND) {
|
|
139
172
|
await this.checkAndCreateDb()
|
|
140
173
|
return await this.performCall(call)
|
|
141
|
-
} else if (err.statusCode) {
|
|
142
|
-
err.status = err.statusCode
|
|
143
174
|
}
|
|
144
|
-
|
|
175
|
+
// stripping the error down the props which are safe/useful, drop everything else
|
|
176
|
+
throw new CouchDBError(`CouchDB error: ${err.message}`, err)
|
|
145
177
|
}
|
|
146
178
|
}
|
|
147
179
|
|
|
@@ -288,7 +320,7 @@ export class DatabaseImpl implements Database {
|
|
|
288
320
|
if (err.statusCode === 404) {
|
|
289
321
|
return
|
|
290
322
|
} else {
|
|
291
|
-
throw
|
|
323
|
+
throw new CouchDBError(err.message, err)
|
|
292
324
|
}
|
|
293
325
|
}
|
|
294
326
|
}
|
|
@@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context"
|
|
|
13
13
|
import { decrypt } from "../security/encryption"
|
|
14
14
|
import * as identity from "../context/identity"
|
|
15
15
|
import env from "../environment"
|
|
16
|
-
import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
|
|
16
|
+
import { Ctx, EndpointMatcher, SessionCookie, User } from "@budibase/types"
|
|
17
17
|
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
|
18
18
|
import tracer from "dd-trace"
|
|
19
19
|
|
|
@@ -41,7 +41,10 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
|
|
|
41
41
|
ctx.version = opts.version
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function checkApiKey(
|
|
44
|
+
async function checkApiKey(
|
|
45
|
+
apiKey: string,
|
|
46
|
+
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
|
47
|
+
) {
|
|
45
48
|
// check both the primary and the fallback internal api keys
|
|
46
49
|
// this allows for rotation
|
|
47
50
|
if (isValidInternalAPIKey(apiKey)) {
|
|
@@ -128,6 +131,7 @@ export default function (
|
|
|
128
131
|
} else {
|
|
129
132
|
user = await getUser(userId, session.tenantId)
|
|
130
133
|
}
|
|
134
|
+
// @ts-ignore
|
|
131
135
|
user.csrfToken = session.csrfToken
|
|
132
136
|
|
|
133
137
|
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
|
@@ -167,19 +171,25 @@ export default function (
|
|
|
167
171
|
authenticated = false
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
|
|
174
|
+
const isUser = (
|
|
175
|
+
user: any
|
|
176
|
+
): user is User & { budibaseAccess?: string } => {
|
|
177
|
+
return user && user.email
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isUser(user)) {
|
|
171
181
|
tracer.setUser({
|
|
172
|
-
id: user
|
|
173
|
-
tenantId: user
|
|
174
|
-
budibaseAccess: user
|
|
175
|
-
status: user
|
|
182
|
+
id: user._id!,
|
|
183
|
+
tenantId: user.tenantId,
|
|
184
|
+
budibaseAccess: user.budibaseAccess,
|
|
185
|
+
status: user.status,
|
|
176
186
|
})
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
// isAuthenticated is a function, so use a variable to be able to check authed state
|
|
180
190
|
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
|
181
191
|
|
|
182
|
-
if (user
|
|
192
|
+
if (isUser(user)) {
|
|
183
193
|
return identity.doInUserContext(user, ctx, next)
|
|
184
194
|
} else {
|
|
185
195
|
return next()
|
|
@@ -13,13 +13,14 @@ import { bucketTTLConfig, budibaseTempDir } from "./utils"
|
|
|
13
13
|
import { v4 } from "uuid"
|
|
14
14
|
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
|
15
15
|
import fsp from "fs/promises"
|
|
16
|
+
import { HeadObjectOutput } from "aws-sdk/clients/s3"
|
|
16
17
|
|
|
17
18
|
const streamPipeline = promisify(stream.pipeline)
|
|
18
19
|
// use this as a temporary store of buckets that are being created
|
|
19
20
|
const STATE = {
|
|
20
21
|
bucketCreationPromises: {},
|
|
21
22
|
}
|
|
22
|
-
const
|
|
23
|
+
export const SIGNED_FILE_PREFIX = "/files/signed"
|
|
23
24
|
|
|
24
25
|
type ListParams = {
|
|
25
26
|
ContinuationToken?: string
|
|
@@ -40,8 +41,13 @@ type UploadParams = BaseUploadParams & {
|
|
|
40
41
|
path?: string | PathLike
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
type
|
|
44
|
-
|
|
44
|
+
export type StreamTypes =
|
|
45
|
+
| ReadStream
|
|
46
|
+
| NodeJS.ReadableStream
|
|
47
|
+
| ReadableStream<Uint8Array>
|
|
48
|
+
|
|
49
|
+
export type StreamUploadParams = BaseUploadParams & {
|
|
50
|
+
stream?: StreamTypes
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const CONTENT_TYPE_MAP: any = {
|
|
@@ -174,11 +180,9 @@ export async function upload({
|
|
|
174
180
|
const objectStore = ObjectStore(bucketName)
|
|
175
181
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
|
176
182
|
|
|
177
|
-
if (ttl &&
|
|
183
|
+
if (ttl && bucketCreated.created) {
|
|
178
184
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
|
179
|
-
|
|
180
|
-
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
|
181
|
-
}
|
|
185
|
+
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
let contentType = type
|
|
@@ -222,11 +226,9 @@ export async function streamUpload({
|
|
|
222
226
|
const objectStore = ObjectStore(bucketName)
|
|
223
227
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
|
224
228
|
|
|
225
|
-
if (ttl &&
|
|
229
|
+
if (ttl && bucketCreated.created) {
|
|
226
230
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
|
227
|
-
|
|
228
|
-
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
|
229
|
-
}
|
|
231
|
+
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
// Set content type for certain known extensions
|
|
@@ -333,7 +335,7 @@ export function getPresignedUrl(
|
|
|
333
335
|
const signedUrl = new URL(url)
|
|
334
336
|
const path = signedUrl.pathname
|
|
335
337
|
const query = signedUrl.search
|
|
336
|
-
return `${
|
|
338
|
+
return `${SIGNED_FILE_PREFIX}${path}${query}`
|
|
337
339
|
}
|
|
338
340
|
}
|
|
339
341
|
|
|
@@ -521,6 +523,26 @@ export async function getReadStream(
|
|
|
521
523
|
return client.getObject(params).createReadStream()
|
|
522
524
|
}
|
|
523
525
|
|
|
526
|
+
export async function getObjectMetadata(
|
|
527
|
+
bucket: string,
|
|
528
|
+
path: string
|
|
529
|
+
): Promise<HeadObjectOutput> {
|
|
530
|
+
bucket = sanitizeBucket(bucket)
|
|
531
|
+
path = sanitizeKey(path)
|
|
532
|
+
|
|
533
|
+
const client = ObjectStore(bucket)
|
|
534
|
+
const params = {
|
|
535
|
+
Bucket: bucket,
|
|
536
|
+
Key: path,
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
return await client.headObject(params).promise()
|
|
541
|
+
} catch (err: any) {
|
|
542
|
+
throw new Error("Unable to retrieve metadata from object")
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
524
546
|
/*
|
|
525
547
|
Given a signed url like '/files/signed/tmp-files-attachments/app_123456/myfile.txt' extract
|
|
526
548
|
the bucket and the path from it
|
|
@@ -530,7 +552,9 @@ export function extractBucketAndPath(
|
|
|
530
552
|
): { bucket: string; path: string } | null {
|
|
531
553
|
const baseUrl = url.split("?")[0]
|
|
532
554
|
|
|
533
|
-
const regex = new RegExp(
|
|
555
|
+
const regex = new RegExp(
|
|
556
|
+
`^${SIGNED_FILE_PREFIX}/(?<bucket>[^/]+)/(?<path>.+)$`
|
|
557
|
+
)
|
|
534
558
|
const match = baseUrl.match(regex)
|
|
535
559
|
|
|
536
560
|
if (match && match.groups) {
|
package/src/objectStore/utils.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { join } from "path"
|
|
1
|
+
import path, { join } from "path"
|
|
2
2
|
import { tmpdir } from "os"
|
|
3
3
|
import fs from "fs"
|
|
4
4
|
import env from "../environment"
|
|
5
5
|
import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"
|
|
6
|
-
|
|
6
|
+
import * as objectStore from "./objectStore"
|
|
7
|
+
import {
|
|
8
|
+
AutomationAttachment,
|
|
9
|
+
AutomationAttachmentContent,
|
|
10
|
+
BucketedContent,
|
|
11
|
+
} from "@budibase/types"
|
|
7
12
|
/****************************************************
|
|
8
13
|
* NOTE: When adding a new bucket - name *
|
|
9
14
|
* sure that S3 usages (like budibase-infra) *
|
|
@@ -55,3 +60,50 @@ export const bucketTTLConfig = (
|
|
|
55
60
|
|
|
56
61
|
return params
|
|
57
62
|
}
|
|
63
|
+
|
|
64
|
+
async function processUrlAttachment(
|
|
65
|
+
attachment: AutomationAttachment
|
|
66
|
+
): Promise<AutomationAttachmentContent> {
|
|
67
|
+
const response = await fetch(attachment.url)
|
|
68
|
+
if (!response.ok || !response.body) {
|
|
69
|
+
throw new Error(`Unexpected response ${response.statusText}`)
|
|
70
|
+
}
|
|
71
|
+
const fallbackFilename = path.basename(new URL(attachment.url).pathname)
|
|
72
|
+
return {
|
|
73
|
+
filename: attachment.filename || fallbackFilename,
|
|
74
|
+
content: response.body,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function processObjectStoreAttachment(
|
|
79
|
+
attachment: AutomationAttachment
|
|
80
|
+
): Promise<BucketedContent> {
|
|
81
|
+
const result = objectStore.extractBucketAndPath(attachment.url)
|
|
82
|
+
|
|
83
|
+
if (result === null) {
|
|
84
|
+
throw new Error("Invalid signed URL")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { bucket, path: objectPath } = result
|
|
88
|
+
const readStream = await objectStore.getReadStream(bucket, objectPath)
|
|
89
|
+
const fallbackFilename = path.basename(objectPath)
|
|
90
|
+
return {
|
|
91
|
+
bucket,
|
|
92
|
+
path: objectPath,
|
|
93
|
+
filename: attachment.filename || fallbackFilename,
|
|
94
|
+
content: readStream,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function processAutomationAttachment(
|
|
99
|
+
attachment: AutomationAttachment
|
|
100
|
+
): Promise<AutomationAttachmentContent | BucketedContent> {
|
|
101
|
+
const isFullyFormedUrl =
|
|
102
|
+
attachment.url?.startsWith("http://") ||
|
|
103
|
+
attachment.url?.startsWith("https://")
|
|
104
|
+
if (isFullyFormedUrl) {
|
|
105
|
+
return await processUrlAttachment(attachment)
|
|
106
|
+
} else {
|
|
107
|
+
return await processObjectStoreAttachment(attachment)
|
|
108
|
+
}
|
|
109
|
+
}
|