Skip to content

Commit 7ea754f

Browse files
committed
preserve project fixes from earlier merge resolutions
1 parent 85f89ba commit 7ea754f

9 files changed

Lines changed: 237 additions & 104 deletions

File tree

‎packages/builder/src/components/integration/APIEndpointViewer.svelte‎

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,10 @@
2727
notifications,
2828
Banner,
2929
Divider,
30-
Label,
3130
Switcher,
3231
} from "@budibase/bbui"
3332
import {
3433
BodyType,
35-
FeatureFlag,
3634
type Query,
3735
type Datasource,
3836
type ImportEndpoint,
@@ -87,14 +85,13 @@
8785
import ExpandablePanel from "@/components/common/ExpandablePanel.svelte"
8886
import ConnectionSelect from "./rest/ConnectionSelect.svelte"
8987
import AccessLevelSelect from "@/components/integration/AccessLevelSelect.svelte"
90-
import ProjectSelect from "@/components/common/ProjectSelect.svelte"
9188
import { getErrorMessage } from "@/helpers/errors"
9289
import { confirm } from "@/helpers"
9390
import {
9491
urlParamHighlightPlugin,
9592
urlParamHighlightTheme,
9693
} from "../common/CodeEditor/urlParamHighlight"
97-
import { environment, featureFlags } from "@/stores/portal"
94+
import { environment } from "@/stores/portal"
9895
import { workspaceConnections } from "@/stores/builder/workspaceConnection"
9996
import { onDestroy, onMount, createEventDispatcher } from "svelte"
10097
@@ -109,11 +106,9 @@
109106
export let connectionPopoverPortalTarget: string | undefined = undefined
110107
export let connectionPopoverZIndex: number | undefined = undefined
111108
export let openAddConnectionOnMount: boolean = false
112-
export let initialProjectIds: string[] = []
113109
114110
$beforeUrlChange
115111
$: goto = $gotoStore
116-
$: projectsEnabled = $featureFlags[FeatureFlag.PROJECTS]
117112
118113
type EndpointWithIcon = ImportEndpoint & {
119114
icon?: {
@@ -141,8 +136,6 @@
141136
let response: PreviewQueryResponse
142137
let panelMode: "response" | "request" = "response"
143138
let editableQuery: Query | undefined
144-
let projectIds: string[] = []
145-
let originalProjectIds: string[] = []
146139
let datasource: Datasource | UIInternalDatasource | undefined
147140
let enabledHeaders: Record<string, boolean> = {}
148141
let globalDynamicRequestBindings: EnrichedBinding[] = []
@@ -221,12 +214,6 @@
221214
222215
$: if (querySourceKey !== lastQuerySourceKey) {
223216
editableQuery = structuredClone(storeQuery)
224-
projectIds =
225-
editableQuery?.projectIds ||
226-
(!editableQuery?._id && initialProjectIds.length
227-
? [...initialProjectIds]
228-
: [])
229-
originalProjectIds = [...projectIds]
230217
lastQuerySourceKey = querySourceKey
231218
queryParams = undefined
232219
originalBuiltQuery = undefined
@@ -335,14 +322,18 @@
335322
? prettifyQueryRequestBody(editableQuery, mergedBindings)
336323
: undefined
337324
325+
const stripQueryProjectIds = (query: Query): Query => {
326+
const { projectIds: _projectIds, ...queryWithoutProjectIds } = query
327+
return queryWithoutProjectIds
328+
}
329+
338330
// ── BUILT QUERY & DIRTY STATE ─────────────────────────────────────────────
339331
$: builtQuery =
340332
editableQuery &&
341333
schema &&
342334
buildQuery(
343335
{
344-
...editableQuery,
345-
projectIds: getQueryProjectIds(),
336+
...stripQueryProjectIds(editableQuery),
346337
datasourceId: selectedDatasourceId || editableQuery.datasourceId,
347338
fields: { ...editableQuery.fields, path: requestUrl },
348339
},
@@ -452,13 +443,6 @@
452443
ds: Datasource | UIInternalDatasource | undefined
453444
): string | undefined => (ds as Datasource)?.config?.url as string | undefined
454445
455-
const getQueryProjectIds = () => {
456-
if (projectIds.length) {
457-
return projectIds
458-
}
459-
return !isNewQuery && originalProjectIds.length ? [] : undefined
460-
}
461-
462446
const resolveStoreQuery = (
463447
list: Query[] | undefined,
464448
qId: string | undefined,
@@ -708,8 +692,6 @@
708692
}
709693
710694
editableQuery = structuredClone(updatedQuery)
711-
projectIds = updatedQuery.projectIds || []
712-
originalProjectIds = [...projectIds]
713695
originalBuiltQuery = undefined
714696
localDynamicVariables = undefined
715697
@@ -1079,12 +1061,6 @@
10791061
<div class="access">
10801062
<AccessLevelSelect query={editableQuery} label="Access" />
10811063
</div>
1082-
{#if projectsEnabled}
1083-
<div class="project">
1084-
<Label>Projects</Label>
1085-
<ProjectSelect bind:value={projectIds} label="" autoWidth />
1086-
</div>
1087-
{/if}
10881064
{/if}
10891065
{#if endpointDocs}
10901066
<ActionButton
@@ -1646,11 +1622,6 @@
16461622
align-items: center;
16471623
gap: var(--spacing-m);
16481624
}
1649-
.project {
1650-
display: flex;
1651-
align-items: center;
1652-
gap: var(--spacing-m);
1653-
}
16541625
.pagination {
16551626
display: grid;
16561627
grid-template-columns: 1fr 1fr;

‎packages/server/src/api/routes/tests/project.spec.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
context,
33
db as dbCore,
4+
encryption,
45
features,
56
ViewName,
67
} from "@budibase/backend-core"
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { context, features } from "@budibase/backend-core"
2+
import { FeatureFlag } from "@budibase/types"
3+
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
4+
import {
5+
basicDatasource,
6+
basicQuery,
7+
basicTable,
8+
createQueryScreen,
9+
createViewScreen,
10+
} from "../../../tests/utilities/structures"
11+
12+
describe("Project package ownership", () => {
13+
const config = new TestConfiguration()
14+
15+
beforeEach(async () => config.newTenant())
16+
afterAll(() => config.end())
17+
18+
it("imports an app after its query datasource was excluded from assignment", async () => {
19+
await features.testutils.withFeatureFlags(
20+
config.getTenantId(),
21+
{ [FeatureFlag.PROJECTS]: true },
22+
async () => {
23+
const { project } = await config.api.project.create({ name: "Support" })
24+
const datasource = await config.api.datasource.create(
25+
basicDatasource().datasource
26+
)
27+
const query = await config.api.query.save(basicQuery(datasource._id!))
28+
const { workspaceApp } = await config.api.workspaceApp.create({
29+
name: "Support app",
30+
url: "/support",
31+
})
32+
await config.api.screen.save({
33+
...createQueryScreen(datasource._id!, query),
34+
workspaceAppId: workspaceApp._id,
35+
})
36+
await config.doInContext(config.getDevWorkspaceId(), async () => {
37+
await context.getWorkspaceDB().put({
38+
...workspaceApp,
39+
projectIds: [project._id],
40+
})
41+
})
42+
43+
const archive = await config.api.project.export(project._id)
44+
const imported = await config.api.project.import(archive)
45+
46+
expect(imported.resources.workspace_app).toHaveLength(1)
47+
expect(imported.resources.query).toBeUndefined()
48+
expect(imported.resources.datasource).toBeUndefined()
49+
}
50+
)
51+
})
52+
53+
it("imports an app after its row action table was excluded from assignment", async () => {
54+
await features.testutils.withFeatureFlags(
55+
config.getTenantId(),
56+
{ [FeatureFlag.PROJECTS]: true },
57+
async () => {
58+
const { project } = await config.api.project.create({ name: "Support" })
59+
const table = await config.api.table.save(basicTable())
60+
await config.api.rowAction.save(table._id!, { name: "Approve" })
61+
const view = await config.api.viewV2.create({
62+
tableId: table._id!,
63+
name: "Open tickets",
64+
})
65+
const { workspaceApp } = await config.api.workspaceApp.create({
66+
name: "Support app",
67+
url: "/support",
68+
})
69+
await config.api.screen.save({
70+
...createViewScreen(view),
71+
workspaceAppId: workspaceApp._id,
72+
})
73+
await config.doInContext(config.getDevWorkspaceId(), async () => {
74+
await context.getWorkspaceDB().put({
75+
...workspaceApp,
76+
projectIds: [project._id],
77+
})
78+
})
79+
80+
const archive = await config.api.project.export(project._id)
81+
const imported = await config.api.project.import(archive)
82+
83+
expect(imported.resources.workspace_app).toHaveLength(1)
84+
expect(imported.resources.row_action).toBeUndefined()
85+
expect(imported.resources.table).toBeUndefined()
86+
expect(imported.resources.automation).toBeUndefined()
87+
}
88+
)
89+
})
90+
})

‎packages/server/src/sdk/workspace/backups/imports.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,11 @@ export async function untarFile(file: { path: string }) {
156156
return tmpPath
157157
}
158158

159-
export async function decryptFiles(path: string, password: string) {
159+
export async function decryptFiles(
160+
path: string,
161+
password: string,
162+
{ maxOutputBytes = Infinity }: { maxOutputBytes?: number } = {}
163+
) {
160164
try {
161165
const processDirectory = async (dirPath: string) => {
162166
for (let file of await fsp.readdir(dirPath)) {
@@ -165,7 +169,10 @@ export async function decryptFiles(path: string, password: string) {
165169
const stats = await fsp.lstat(inputPath)
166170
if (stats.isFile() && inputPath.endsWith(".enc")) {
167171
const outputPath = inputPath.replace(/\.enc$/, "")
168-
await encryption.decryptFile(inputPath, outputPath, password)
172+
await encryption.decryptFile(inputPath, outputPath, password, {
173+
maxOutputBytes,
174+
})
175+
maxOutputBytes -= (await fsp.stat(outputPath)).size
169176
await fsp.rm(inputPath)
170177
} else if (stats.isDirectory()) {
171178
await processDirectory(inputPath)

‎packages/server/src/sdk/workspace/projects/backups/constants.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ export const PROJECT_FILE = "project.json"
44
export const PROJECT_DEPENDENCY_INDEX_FILE = "dependency-index.json"
55
export const PROJECT_DOCS_DIRECTORY = "docs"
66
export const PROJECT_ATTACHMENTS_DIRECTORY = "attachments"
7+
export const MAX_PROJECT_ARCHIVE_SIZE_BYTES = 50 * 1024 * 1024
8+
export const MAX_PROJECT_EXTRACTED_SIZE_BYTES = 100 * 1024 * 1024
9+
export const MAX_PROJECT_PACKAGE_FILES = 2000

‎packages/server/src/sdk/workspace/projects/backups/exports.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { context, encryption } from "@budibase/backend-core"
1+
import { context, encryption, HTTPError } from "@budibase/backend-core"
22
import {
33
Agent,
44
AnyDocument,
@@ -29,13 +29,15 @@ import {
2929
isDisallowedProjectAssignmentResourceId,
3030
} from "../../resources/utils"
3131
import {
32+
MAX_PROJECT_ARCHIVE_SIZE_BYTES,
3233
PROJECT_ATTACHMENTS_DIRECTORY,
3334
PROJECT_DEPENDENCY_INDEX_FILE,
3435
PROJECT_DOCS_DIRECTORY,
3536
PROJECT_EXPORT_FORMAT_VERSION,
3637
PROJECT_FILE,
3738
PROJECT_MANIFEST_FILE,
3839
} from "./constants"
40+
import { readProjectPackageFiles } from "./files"
3941
import { doWithProjectAssignmentsLock } from "../lock"
4042
import {
4143
fetchAssignedProjectDocs,
@@ -514,11 +516,18 @@ export async function exportProject(
514516
throw writeFailure.reason
515517
}
516518

519+
await readProjectPackageFiles({ dirPath: tmpPath })
520+
517521
if (opts?.encryptPassword) {
518522
await encryptDirectory(tmpPath, opts.encryptPassword)
519523
}
520524

521-
return await tarFilesToTmp(tmpPath, await fsp.readdir(tmpPath))
525+
const tarPath = await tarFilesToTmp(tmpPath, await fsp.readdir(tmpPath))
526+
if ((await fsp.stat(tarPath)).size > MAX_PROJECT_ARCHIVE_SIZE_BYTES) {
527+
await fsp.rm(tarPath, { force: true })
528+
throw new HTTPError("Project package is too large.", 400)
529+
}
530+
return tarPath
522531
} finally {
523532
await fsp.rm(tmpPath, { recursive: true, force: true })
524533
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { HTTPError } from "@budibase/backend-core"
2+
import fsp from "fs/promises"
3+
import { join, relative } from "path"
4+
import {
5+
MAX_PROJECT_EXTRACTED_SIZE_BYTES,
6+
MAX_PROJECT_PACKAGE_FILES,
7+
} from "./constants"
8+
9+
export const MAX_PROJECT_PATH_SEGMENTS = 4
10+
11+
export const isSafeArchivePath = (path: string) => {
12+
const segments = path.split(/[\\/]/)
13+
return (
14+
!path.startsWith("/") &&
15+
!path.startsWith("\\") &&
16+
!/^[A-Za-z]:/.test(path) &&
17+
segments.every(segment => segment !== ".." && segment !== ".")
18+
)
19+
}
20+
21+
export const readProjectPackageFiles = async ({
22+
dirPath,
23+
rootPath = dirPath,
24+
totals = { files: 0, bytes: 0 },
25+
}: {
26+
dirPath: string
27+
rootPath?: string
28+
totals?: { files: number; bytes: number }
29+
}): Promise<string[]> => {
30+
const entries = await fsp.readdir(dirPath, { withFileTypes: true })
31+
const files: string[] = []
32+
33+
for (const entry of entries) {
34+
const fullPath = join(dirPath, entry.name)
35+
const relPath = relative(rootPath, fullPath)
36+
if (!isSafeArchivePath(relPath)) {
37+
throw new HTTPError("Project package contains unsafe paths.", 400)
38+
}
39+
if (relPath.split(/[\\/]/).length > MAX_PROJECT_PATH_SEGMENTS) {
40+
throw new HTTPError(
41+
"Project package contains paths that are too deep.",
42+
400
43+
)
44+
}
45+
if (entry.isSymbolicLink()) {
46+
throw new HTTPError("Project package contains unsupported links.", 400)
47+
}
48+
if (entry.isDirectory()) {
49+
files.push(
50+
...(await readProjectPackageFiles({
51+
dirPath: fullPath,
52+
rootPath,
53+
totals,
54+
}))
55+
)
56+
} else {
57+
const stats = await fsp.stat(fullPath)
58+
totals.files += 1
59+
totals.bytes += stats.size
60+
if (totals.files > MAX_PROJECT_PACKAGE_FILES) {
61+
throw new HTTPError("Project package contains too many files.", 400)
62+
}
63+
if (totals.bytes > MAX_PROJECT_EXTRACTED_SIZE_BYTES) {
64+
throw new HTTPError("Project package is too large.", 400)
65+
}
66+
files.push(fullPath)
67+
}
68+
}
69+
70+
return files
71+
}

0 commit comments

Comments
 (0)