@budibase/worker 3.12.5 → 3.12.7

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/nodemon.json CHANGED
@@ -5,7 +5,8 @@
5
5
  "../pro",
6
6
  "../types",
7
7
  "../shared-core",
8
- "../string-templates"
8
+ "../string-templates",
9
+ "../../.env"
9
10
  ],
10
11
  "ext": "js,ts,json",
11
12
  "ignore": ["**/*.spec.ts", "**/*.spec.js", "../*/dist/**/*"],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "3.12.5",
4
+ "version": "3.12.7",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -21,19 +21,10 @@
21
21
  "run:docker": "node dist/index.js",
22
22
  "debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js",
23
23
  "run:docker:cluster": "pm2-runtime start pm2.config.js",
24
- "dev:stack:init": "node ./scripts/dev/manage.js init",
25
- "dev": "npm run dev:stack:init && nodemon",
26
- "dev:built": "yarn run dev:stack:init && yarn run run:docker",
24
+ "dev": "nodemon",
25
+ "dev:built": "yarn run run:docker",
27
26
  "test": "bash scripts/test.sh",
28
- "test:watch": "jest --watch",
29
- "env:multi:enable": "node scripts/multiTenancy.js enable",
30
- "env:multi:disable": "node scripts/multiTenancy.js disable",
31
- "env:selfhost:enable": "node scripts/selfhost.js enable",
32
- "env:selfhost:disable": "node scripts/selfhost.js disable",
33
- "env:localdomain:enable": "node scripts/localdomain.js enable",
34
- "env:localdomain:disable": "node scripts/localdomain.js disable",
35
- "env:account:enable": "node scripts/account.js enable",
36
- "env:account:disable": "node scripts/account.js disable"
27
+ "test:watch": "jest --watch"
37
28
  },
38
29
  "author": "Budibase",
39
30
  "license": "GPL-3.0",
@@ -102,8 +93,7 @@
102
93
  "superagent": "^10.1.1",
103
94
  "supertest": "6.3.3",
104
95
  "timekeeper": "2.2.0",
105
- "typescript": "5.7.2",
106
- "update-dotenv": "1.1.1"
96
+ "typescript": "5.7.2"
107
97
  },
108
98
  "resolutions": {
109
99
  "@budibase/pro": "npm:@budibase/pro@latest"
@@ -123,5 +113,5 @@
123
113
  }
124
114
  }
125
115
  },
126
- "gitHead": "1b7296dc708223ebfa03d91b471c1ad4a4b49758"
116
+ "gitHead": "87d0fd4e2bfa88a5f45312ce133670856957b97b"
127
117
  }
@@ -13,7 +13,6 @@ import {
13
13
  } from "@budibase/backend-core"
14
14
  import { checkAnyUserExists } from "../../../utilities/users"
15
15
  import {
16
- AIConfig,
17
16
  AIInnerConfig,
18
17
  Config,
19
18
  ConfigChecklistResponse,
@@ -36,6 +35,7 @@ import {
36
35
  SaveConfigResponse,
37
36
  SettingsBrandingConfig,
38
37
  SettingsInnerConfig,
38
+ SMTPInnerConfig,
39
39
  SSOConfig,
40
40
  SSOConfigType,
41
41
  UploadConfigFileResponse,
@@ -158,7 +158,23 @@ async function hasActivatedConfig(ssoConfigs?: SSOConfigs) {
158
158
  return !!Object.values(ssoConfigs).find(c => c?.activated)
159
159
  }
160
160
 
161
- async function verifySettingsConfig(
161
+ async function processSMTPConfig(
162
+ config: SMTPInnerConfig,
163
+ existingConfig?: SMTPInnerConfig
164
+ ) {
165
+ await email.verifyConfig(config)
166
+ if (config.auth?.pass === PASSWORD_REPLACEMENT) {
167
+ // if the password is being replaced, use the existing password
168
+ if (existingConfig && existingConfig.auth?.pass) {
169
+ config.auth.pass = existingConfig.auth.pass
170
+ } else {
171
+ // otherwise, throw an error
172
+ throw new BadRequestError("SMTP password is required")
173
+ }
174
+ }
175
+ }
176
+
177
+ async function processSettingsConfig(
162
178
  config: SettingsInnerConfig & SettingsBrandingConfig,
163
179
  existingConfig?: SettingsInnerConfig & SettingsBrandingConfig
164
180
  ) {
@@ -203,19 +219,37 @@ async function verifySSOConfig(type: SSOConfigType, config: SSOConfig) {
203
219
  }
204
220
  }
205
221
 
206
- async function verifyGoogleConfig(config: GoogleInnerConfig) {
222
+ async function processGoogleConfig(
223
+ config: GoogleInnerConfig,
224
+ existing?: GoogleInnerConfig
225
+ ) {
207
226
  await verifySSOConfig(ConfigType.GOOGLE, config)
227
+
228
+ if (existing && config.clientSecret === PASSWORD_REPLACEMENT) {
229
+ config.clientSecret = existing.clientSecret
230
+ }
208
231
  }
209
232
 
210
- async function verifyOIDCConfig(config: OIDCConfigs) {
233
+ async function processOIDCConfig(config: OIDCConfigs, existing?: OIDCConfigs) {
211
234
  await verifySSOConfig(ConfigType.OIDC, config.configs[0])
235
+
236
+ if (existing) {
237
+ for (const c of config.configs) {
238
+ const existingConfig = existing.configs.find(e => e.uuid === c.uuid)
239
+ if (!existingConfig) {
240
+ continue
241
+ }
242
+ if (c.clientSecret === PASSWORD_REPLACEMENT) {
243
+ c.clientSecret = existingConfig.clientSecret
244
+ }
245
+ }
246
+ }
212
247
  }
213
248
 
214
249
  export async function processAIConfig(
215
250
  newConfig: AIInnerConfig,
216
251
  existingConfig: AIInnerConfig
217
252
  ) {
218
- // ensure that the redacted API keys are not overwritten in the DB
219
253
  for (const key in existingConfig) {
220
254
  if (newConfig[key]?.apiKey === PASSWORD_REPLACEMENT) {
221
255
  newConfig[key].apiKey = existingConfig[key].apiKey
@@ -256,16 +290,16 @@ export async function save(
256
290
  try {
257
291
  switch (type) {
258
292
  case ConfigType.SMTP:
259
- await email.verifyConfig(config)
293
+ await processSMTPConfig(config, existingConfig?.config)
260
294
  break
261
295
  case ConfigType.SETTINGS:
262
- await verifySettingsConfig(config, existingConfig?.config)
296
+ await processSettingsConfig(config, existingConfig?.config)
263
297
  break
264
298
  case ConfigType.GOOGLE:
265
- await verifyGoogleConfig(config)
299
+ await processGoogleConfig(config, existingConfig?.config)
266
300
  break
267
301
  case ConfigType.OIDC:
268
- await verifyOIDCConfig(config)
302
+ await processOIDCConfig(config, existingConfig?.config)
269
303
  break
270
304
  case ConfigType.AI:
271
305
  if (existingConfig) {
@@ -353,45 +387,48 @@ async function enrichOIDCLogos(oidcLogos: OIDCLogosConfig) {
353
387
  }
354
388
 
355
389
  export async function find(ctx: UserCtx<void, FindConfigResponse>) {
356
- try {
357
- // Find the config with the most granular scope based on context
358
- const type = ctx.params.type
359
- let scopedConfig = await configs.getConfig(type)
360
-
361
- if (scopedConfig) {
362
- await handleConfigType(type, scopedConfig)
363
- } else if (type === ConfigType.AI) {
364
- scopedConfig = { type: ConfigType.AI, config: {} }
365
- await handleAIConfig(scopedConfig)
366
- } else {
367
- // If no config found and not AI type, just return an empty body
368
- ctx.body = {}
369
- return
370
- }
390
+ // Find the config with the most granular scope based on context
391
+ const type = ctx.params.type
392
+ let config = await configs.getConfig(type)
371
393
 
372
- ctx.body = scopedConfig
373
- } catch (err: any) {
374
- ctx.throw(err?.status || 400, err)
394
+ if (!config && type === ConfigType.AI) {
395
+ config = { type: ConfigType.AI, config: {} }
396
+ }
397
+
398
+ if (!config) {
399
+ ctx.body = {}
400
+ return
375
401
  }
376
- }
377
402
 
378
- async function handleConfigType(type: ConfigType, config: Config) {
379
- if (type === ConfigType.OIDC_LOGOS) {
380
- await enrichOIDCLogos(config)
381
- } else if (type === ConfigType.AI) {
382
- await handleAIConfig(config)
403
+ switch (type) {
404
+ case ConfigType.OIDC_LOGOS:
405
+ await enrichOIDCLogos(config)
406
+ break
407
+ case ConfigType.AI:
408
+ await pro.sdk.ai.enrichAIConfig(config)
409
+ break
383
410
  }
384
- }
385
411
 
386
- async function handleAIConfig(config: AIConfig) {
387
- await pro.sdk.ai.enrichAIConfig(config)
388
- stripApiKeys(config)
412
+ stripSecrets(config)
413
+ ctx.body = config
389
414
  }
390
415
 
391
- function stripApiKeys(config: AIConfig) {
392
- for (const key in config?.config) {
393
- if (config.config[key].apiKey) {
394
- config.config[key].apiKey = PASSWORD_REPLACEMENT
416
+ function stripSecrets(config: Config) {
417
+ if (isAIConfig(config)) {
418
+ for (const key in config.config) {
419
+ if (config.config[key].apiKey) {
420
+ config.config[key].apiKey = PASSWORD_REPLACEMENT
421
+ }
422
+ }
423
+ } else if (isSMTPConfig(config)) {
424
+ if (config.config.auth?.pass) {
425
+ config.config.auth.pass = PASSWORD_REPLACEMENT
426
+ }
427
+ } else if (isGoogleConfig(config)) {
428
+ config.config.clientSecret = PASSWORD_REPLACEMENT
429
+ } else if (isOIDCConfig(config)) {
430
+ for (const c of config.config.configs) {
431
+ c.clientSecret = PASSWORD_REPLACEMENT
395
432
  }
396
433
  }
397
434
  }
@@ -33,6 +33,7 @@ import {
33
33
  SaveUserResponse,
34
34
  SearchUsersRequest,
35
35
  SearchUsersResponse,
36
+ StrippedUser,
36
37
  UnsavedUser,
37
38
  UpdateInviteRequest,
38
39
  UpdateInviteResponse,
@@ -66,6 +67,15 @@ const generatePassword = (length: number) => {
66
67
  .slice(0, length)
67
68
  }
68
69
 
70
+ const stripUsers = (users: (User | StrippedUser)[]): StrippedUser[] => {
71
+ return users.map(user => ({
72
+ _id: user._id,
73
+ email: user.email,
74
+ tenantId: user.tenantId,
75
+ userId: user.userId,
76
+ }))
77
+ }
78
+
69
79
  export const save = async (ctx: UserCtx<UnsavedUser, SaveUserResponse>) => {
70
80
  try {
71
81
  const currentUserId = ctx.user?._id
@@ -249,18 +259,8 @@ export const destroy = async (ctx: UserCtx<void, DeleteUserResponse>) => {
249
259
  }
250
260
  }
251
261
 
252
- export const getAppUsers = async (ctx: Ctx<SearchUsersRequest>) => {
253
- const body = ctx.request.body
254
- const users = await userSdk.db.getUsersByAppAccess({
255
- appId: body.appId,
256
- limit: body.limit,
257
- })
258
-
259
- ctx.body = { data: users }
260
- }
261
-
262
262
  export const search = async (
263
- ctx: Ctx<SearchUsersRequest, SearchUsersResponse>
263
+ ctx: UserCtx<SearchUsersRequest, SearchUsersResponse>
264
264
  ) => {
265
265
  const body = ctx.request.body
266
266
 
@@ -287,8 +287,13 @@ export const search = async (
287
287
  }
288
288
  }
289
289
 
290
+ let response: SearchUsersResponse = { data: [] }
291
+
290
292
  if (body.paginate === false) {
291
- await getAppUsers(ctx)
293
+ response.data = await userSdk.db.getUsersByAppAccess({
294
+ appId: body.appId,
295
+ limit: body.limit,
296
+ })
292
297
  } else {
293
298
  const paginated = await userSdk.core.paginatedUsers(body)
294
299
  // user hashed password shouldn't ever be returned
@@ -297,8 +302,18 @@ export const search = async (
297
302
  delete user.password
298
303
  }
299
304
  }
300
- ctx.body = paginated
305
+ response = {
306
+ data: paginated.data,
307
+ hasNextPage: paginated.hasNextPage,
308
+ nextPage: paginated.nextPage,
309
+ }
301
310
  }
311
+
312
+ if (!users.hasBuilderPermissions(ctx.user)) {
313
+ response.data = stripUsers(response.data)
314
+ }
315
+
316
+ ctx.body = response
302
317
  }
303
318
 
304
319
  // called internally by app server user fetch
@@ -1,19 +1,17 @@
1
- // mock the email system
2
1
  jest.mock("nodemailer")
3
2
  import { TestConfiguration, structures, mocks } from "../../../../tests"
4
3
 
5
4
  mocks.email.mock()
6
- import { events } from "@budibase/backend-core"
5
+ import { configs, events } from "@budibase/backend-core"
7
6
  import { GetPublicSettingsResponse, Config, ConfigType } from "@budibase/types"
8
7
 
8
+ const { google, smtp, settings, oidc } = structures.configs
9
+
9
10
  describe("configs", () => {
10
11
  const config = new TestConfiguration()
11
12
 
12
13
  beforeEach(async () => {
13
14
  await config.beforeAll()
14
- })
15
-
16
- beforeEach(() => {
17
15
  jest.clearAllMocks()
18
16
  })
19
17
 
@@ -22,38 +20,20 @@ describe("configs", () => {
22
20
  })
23
21
 
24
22
  const saveConfig = async (conf: Config, _id?: string, _rev?: string) => {
25
- const data = {
26
- ...conf,
27
- _id,
28
- _rev,
29
- }
23
+ const data = { ...conf, _id, _rev }
30
24
  const res = await config.api.configs.saveConfig(data)
31
25
  return { ...data, ...res }
32
26
  }
33
27
 
34
- const saveSettingsConfig = async (
35
- conf?: any,
36
- _id?: string,
37
- _rev?: string
38
- ) => {
39
- const settingsConfig = structures.configs.settings(conf)
40
- return saveConfig(settingsConfig, _id, _rev)
41
- }
42
-
43
28
  describe("POST /api/global/configs", () => {
44
29
  describe("google", () => {
45
- const saveGoogleConfig = async (
46
- conf?: any,
47
- _id?: string,
48
- _rev?: string
49
- ) => {
50
- const googleConfig = structures.configs.google(conf)
51
- return saveConfig(googleConfig, _id, _rev)
52
- }
30
+ afterEach(async () => {
31
+ await config.deleteConfig(ConfigType.GOOGLE)
32
+ })
53
33
 
54
34
  describe("create", () => {
55
35
  it("should create activated google config", async () => {
56
- await saveGoogleConfig()
36
+ await saveConfig(google())
57
37
  expect(events.auth.SSOCreated).toHaveBeenCalledTimes(1)
58
38
  expect(events.auth.SSOCreated).toHaveBeenCalledWith(ConfigType.GOOGLE)
59
39
  expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
@@ -61,25 +41,23 @@ describe("configs", () => {
61
41
  expect(events.auth.SSOActivated).toHaveBeenCalledWith(
62
42
  ConfigType.GOOGLE
63
43
  )
64
- await config.deleteConfig(ConfigType.GOOGLE)
65
44
  })
66
45
 
67
46
  it("should create deactivated google config", async () => {
68
- await saveGoogleConfig({ activated: false })
47
+ await saveConfig(google({ activated: false }))
69
48
  expect(events.auth.SSOCreated).toHaveBeenCalledTimes(1)
70
49
  expect(events.auth.SSOCreated).toHaveBeenCalledWith(ConfigType.GOOGLE)
71
50
  expect(events.auth.SSOActivated).not.toHaveBeenCalled()
72
51
  expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
73
- await config.deleteConfig(ConfigType.GOOGLE)
74
52
  })
75
53
  })
76
54
 
77
55
  describe("update", () => {
78
56
  it("should update google config to deactivated", async () => {
79
- const googleConf = await saveGoogleConfig()
57
+ const googleConf = await saveConfig(google())
80
58
  jest.clearAllMocks()
81
- await saveGoogleConfig(
82
- { ...googleConf.config, activated: false },
59
+ await saveConfig(
60
+ google({ activated: false }),
83
61
  googleConf._id,
84
62
  googleConf._rev
85
63
  )
@@ -90,14 +68,13 @@ describe("configs", () => {
90
68
  expect(events.auth.SSODeactivated).toHaveBeenCalledWith(
91
69
  ConfigType.GOOGLE
92
70
  )
93
- await config.deleteConfig(ConfigType.GOOGLE)
94
71
  })
95
72
 
96
73
  it("should update google config to activated", async () => {
97
- const googleConf = await saveGoogleConfig({ activated: false })
74
+ const googleConf = await saveConfig(google({ activated: false }))
98
75
  jest.clearAllMocks()
99
- await saveGoogleConfig(
100
- { ...googleConf.config, activated: true },
76
+ await saveConfig(
77
+ google({ activated: true }),
101
78
  googleConf._id,
102
79
  googleConf._rev
103
80
  )
@@ -108,48 +85,64 @@ describe("configs", () => {
108
85
  expect(events.auth.SSOActivated).toHaveBeenCalledWith(
109
86
  ConfigType.GOOGLE
110
87
  )
111
- await config.deleteConfig(ConfigType.GOOGLE)
88
+ })
89
+
90
+ it("should not overwrite secret when updating google config", async () => {
91
+ await saveConfig(google({ clientSecret: "spooky" }))
92
+
93
+ const conf = await config.api.configs.getConfig(ConfigType.GOOGLE)
94
+ await saveConfig(conf)
95
+
96
+ await config.doInTenant(async () => {
97
+ const rawConf = await configs.getGoogleConfig()
98
+ expect(rawConf!.clientSecret).toEqual("spooky")
99
+ })
100
+ })
101
+ })
102
+
103
+ describe("get", () => {
104
+ it("should not leak credentials", async () => {
105
+ await saveConfig(google())
106
+ const conf = await config.api.configs.getConfig(ConfigType.GOOGLE)
107
+ expect(conf.config.clientSecret).toEqual("--secret-value--")
112
108
  })
113
109
  })
114
110
  })
115
111
 
116
112
  describe("oidc", () => {
117
- const saveOIDCConfig = async (
118
- conf?: any,
119
- _id?: string,
120
- _rev?: string
121
- ) => {
122
- const oidcConfig = structures.configs.oidc(conf)
123
- return saveConfig(oidcConfig, _id, _rev)
124
- }
113
+ beforeEach(async () => {
114
+ await config.deleteConfig(ConfigType.OIDC)
115
+ })
116
+
117
+ afterEach(async () => {
118
+ await config.deleteConfig(ConfigType.OIDC)
119
+ })
125
120
 
126
121
  describe("create", () => {
127
122
  it("should create activated OIDC config", async () => {
128
- await saveOIDCConfig()
123
+ await saveConfig(oidc())
129
124
  expect(events.auth.SSOCreated).toHaveBeenCalledTimes(1)
130
125
  expect(events.auth.SSOCreated).toHaveBeenCalledWith(ConfigType.OIDC)
131
126
  expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
132
127
  expect(events.auth.SSOActivated).toHaveBeenCalledTimes(1)
133
128
  expect(events.auth.SSOActivated).toHaveBeenCalledWith(ConfigType.OIDC)
134
- await config.deleteConfig(ConfigType.OIDC)
135
129
  })
136
130
 
137
131
  it("should create deactivated OIDC config", async () => {
138
- await saveOIDCConfig({ activated: false })
132
+ await saveConfig(oidc({ activated: false }))
139
133
  expect(events.auth.SSOCreated).toHaveBeenCalledTimes(1)
140
134
  expect(events.auth.SSOCreated).toHaveBeenCalledWith(ConfigType.OIDC)
141
135
  expect(events.auth.SSOActivated).not.toHaveBeenCalled()
142
136
  expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
143
- await config.deleteConfig(ConfigType.OIDC)
144
137
  })
145
138
  })
146
139
 
147
140
  describe("update", () => {
148
141
  it("should update OIDC config to deactivated", async () => {
149
- const oidcConf = await saveOIDCConfig()
142
+ const oidcConf = await saveConfig(oidc())
150
143
  jest.clearAllMocks()
151
- await saveOIDCConfig(
152
- { ...oidcConf.config.configs[0], activated: false },
144
+ await saveConfig(
145
+ oidc({ activated: false }),
153
146
  oidcConf._id,
154
147
  oidcConf._rev
155
148
  )
@@ -160,14 +153,13 @@ describe("configs", () => {
160
153
  expect(events.auth.SSODeactivated).toHaveBeenCalledWith(
161
154
  ConfigType.OIDC
162
155
  )
163
- await config.deleteConfig(ConfigType.OIDC)
164
156
  })
165
157
 
166
158
  it("should update OIDC config to activated", async () => {
167
- const oidcConf = await saveOIDCConfig({ activated: false })
159
+ const oidcConf = await saveConfig(oidc({ activated: false }))
168
160
  jest.clearAllMocks()
169
- await saveOIDCConfig(
170
- { ...oidcConf.config.configs[0], activated: true },
161
+ await saveConfig(
162
+ oidc({ activated: true }),
171
163
  oidcConf._id,
172
164
  oidcConf._rev
173
165
  )
@@ -176,50 +168,88 @@ describe("configs", () => {
176
168
  expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
177
169
  expect(events.auth.SSOActivated).toHaveBeenCalledTimes(1)
178
170
  expect(events.auth.SSOActivated).toHaveBeenCalledWith(ConfigType.OIDC)
179
- await config.deleteConfig(ConfigType.OIDC)
171
+ })
172
+
173
+ it("should not overwrite secret when updating OIDC config", async () => {
174
+ await saveConfig(oidc({ clientSecret: "spooky" }))
175
+ const conf = await config.api.configs.getConfig(ConfigType.OIDC)
176
+ await saveConfig(conf)
177
+ await config.doInTenant(async () => {
178
+ const rawConf = await configs.getOIDCConfig()
179
+ expect(rawConf!.clientSecret).toEqual("spooky")
180
+ })
181
+ })
182
+ })
183
+
184
+ describe("get", () => {
185
+ it("should not leak credentials", async () => {
186
+ await saveConfig(oidc({ clientSecret: "spooky" }))
187
+ const conf = await config.api.configs.getConfig(ConfigType.OIDC)
188
+ expect(conf.config.configs[0].clientSecret).toEqual(
189
+ "--secret-value--"
190
+ )
180
191
  })
181
192
  })
182
193
  })
183
194
 
184
195
  describe("smtp", () => {
185
- const saveSMTPConfig = async (
186
- conf?: any,
187
- _id?: string,
188
- _rev?: string
189
- ) => {
190
- const smtpConfig = structures.configs.smtp(conf)
191
- return saveConfig(smtpConfig, _id, _rev)
192
- }
196
+ beforeEach(async () => {
197
+ await config.deleteConfig(ConfigType.SMTP)
198
+ })
199
+
200
+ afterEach(async () => {
201
+ await config.deleteConfig(ConfigType.SMTP)
202
+ })
193
203
 
194
204
  describe("create", () => {
195
205
  it("should create SMTP config", async () => {
196
- await config.deleteConfig(ConfigType.SMTP)
197
- await saveSMTPConfig()
206
+ await saveConfig(smtp())
198
207
  expect(events.email.SMTPUpdated).not.toHaveBeenCalled()
199
208
  expect(events.email.SMTPCreated).toHaveBeenCalledTimes(1)
200
- await config.deleteConfig(ConfigType.SMTP)
201
209
  })
202
210
  })
203
211
 
204
212
  describe("update", () => {
205
213
  it("should update SMTP config", async () => {
206
- const smtpConf = await saveSMTPConfig()
214
+ const smtpConf = await saveConfig(smtp())
207
215
  jest.clearAllMocks()
208
- await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev)
216
+ await saveConfig(smtp({ secure: true }), smtpConf._id, smtpConf._rev)
209
217
  expect(events.email.SMTPCreated).not.toHaveBeenCalled()
210
218
  expect(events.email.SMTPUpdated).toHaveBeenCalledTimes(1)
211
- await config.deleteConfig(ConfigType.SMTP)
219
+ })
220
+
221
+ it("should not overwrite secret when updating SMTP config", async () => {
222
+ await saveConfig(smtp({ auth: { user: "jeff", pass: "spooky" } }))
223
+ const conf = await config.api.configs.getConfig(ConfigType.SMTP)
224
+ await saveConfig(conf)
225
+ await config.doInTenant(async () => {
226
+ const rawConf = await configs.getSMTPConfig()
227
+ expect(rawConf!.auth!.pass).toEqual("spooky")
228
+ })
229
+ })
230
+ })
231
+
232
+ describe("get", () => {
233
+ it("should not leak credentials", async () => {
234
+ await saveConfig(smtp({ auth: { user: "jeff", pass: "spooky" } }))
235
+ const conf = await config.api.configs.getConfig(ConfigType.SMTP)
236
+ expect(conf.config.auth!.pass).toEqual("--secret-value--")
212
237
  })
213
238
  })
214
239
  })
215
240
 
216
241
  describe("settings", () => {
217
- describe("create", () => {
218
- it("should create settings config with default settings", async () => {
219
- await config.deleteConfig(ConfigType.SETTINGS)
242
+ beforeEach(async () => {
243
+ await config.deleteConfig(ConfigType.SETTINGS)
244
+ })
220
245
 
221
- await saveSettingsConfig()
246
+ afterEach(async () => {
247
+ await config.deleteConfig(ConfigType.SETTINGS)
248
+ })
222
249
 
250
+ describe("create", () => {
251
+ it("should create settings config with default settings", async () => {
252
+ await saveConfig(settings())
223
253
  expect(events.org.nameUpdated).not.toHaveBeenCalled()
224
254
  expect(events.org.logoUpdated).not.toHaveBeenCalled()
225
255
  expect(events.org.platformURLUpdated).not.toHaveBeenCalled()
@@ -234,7 +264,7 @@ describe("configs", () => {
234
264
  platformUrl: "http://example.com",
235
265
  }
236
266
 
237
- await saveSettingsConfig(conf)
267
+ await saveConfig(settings(conf))
238
268
 
239
269
  expect(events.org.nameUpdated).toHaveBeenCalledTimes(1)
240
270
  expect(events.org.logoUpdated).toHaveBeenCalledTimes(1)
@@ -247,13 +277,13 @@ describe("configs", () => {
247
277
  it("should update settings config", async () => {
248
278
  config.selfHosted()
249
279
  await config.deleteConfig(ConfigType.SETTINGS)
250
- const settingsConfig = await saveSettingsConfig()
280
+ const settingsConfig = await saveConfig(settings())
251
281
  settingsConfig.config.company = "acme"
252
282
  settingsConfig.config.logoUrl = "http://example.com"
253
283
  settingsConfig.config.platformUrl = "http://example.com"
254
284
 
255
- await saveSettingsConfig(
256
- settingsConfig.config,
285
+ await saveConfig(
286
+ settingsConfig,
257
287
  settingsConfig._id,
258
288
  settingsConfig._rev
259
289
  )
@@ -282,16 +312,24 @@ describe("configs", () => {
282
312
  })
283
313
 
284
314
  describe("GET /api/global/configs/public", () => {
315
+ beforeEach(async () => {
316
+ await config.deleteConfig(ConfigType.SETTINGS)
317
+ })
318
+
319
+ afterEach(async () => {
320
+ await config.deleteConfig(ConfigType.SETTINGS)
321
+ })
322
+
285
323
  it("should return the expected public settings", async () => {
286
- await saveSettingsConfig()
324
+ await saveConfig(settings())
287
325
  mocks.pro.features.isSSOEnforced.mockResolvedValue(false)
288
326
 
289
327
  const res = await config.api.configs.getPublicSettings()
290
328
  const body = res.body as GetPublicSettingsResponse
291
329
 
292
330
  const expected = {
293
- _id: "config_settings",
294
- type: "settings",
331
+ _id: `config_${ConfigType.SETTINGS}`,
332
+ type: ConfigType.SETTINGS,
295
333
  config: {
296
334
  company: "Budibase",
297
335
  emailBrandingEnabled: true,
@@ -704,6 +704,24 @@ describe("/api/global/users", () => {
704
704
  expect(response.body.data.length).toBe(1)
705
705
  expect(response.body.data[0].email).toBe(user.email)
706
706
  })
707
+
708
+ it("should strip users if accessing as an end user", async () => {
709
+ const user = await config.createUser({
710
+ admin: { global: false },
711
+ builder: { global: false },
712
+ })
713
+ const response = await config.api.users.searchUsers(
714
+ {
715
+ query: {},
716
+ },
717
+ { useHeaders: await config.login(user) }
718
+ )
719
+ for (let user of response.body.data) {
720
+ expect(user.roles).toBeUndefined()
721
+ expect(user.builder).toBeUndefined()
722
+ expect(user.admin).toBeUndefined()
723
+ }
724
+ })
707
725
  })
708
726
 
709
727
  describe("DELETE /api/global/users/:userId", () => {
@@ -1,14 +1,18 @@
1
1
  import { env as coreEnv } from "@budibase/backend-core"
2
2
  import { ServiceType } from "@budibase/types"
3
- import { join } from "path"
3
+ import { join, resolve } from "path"
4
4
  import cloneDeep from "lodash/cloneDeep"
5
5
 
6
6
  coreEnv._set("SERVICE_TYPE", ServiceType.WORKER)
7
7
 
8
+ const TOP_LEVEL_PATH =
9
+ process.env.TOP_LEVEL_PATH ||
10
+ process.env.WORKER_TOP_LEVEL_PATH ||
11
+ resolve(join(__dirname, "..", "..", ".."))
8
12
  let LOADED = false
9
13
  if (!LOADED && coreEnv.isDev() && !coreEnv.isTest()) {
10
14
  require("dotenv").config({
11
- path: join(__dirname, "..", ".env"),
15
+ path: join(TOP_LEVEL_PATH, ".env"),
12
16
  })
13
17
  LOADED = true
14
18
  }
@@ -40,6 +44,7 @@ const environment = {
40
44
  // prefer worker port to generic port
41
45
  PORT: process.env.WORKER_PORT || process.env.PORT,
42
46
  CLUSTER_PORT: process.env.CLUSTER_PORT,
47
+ WORKER_SERVICE: process.env.WORKER_SERVICE,
43
48
  // flags
44
49
  NODE_ENV: process.env.NODE_ENV,
45
50
  SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
@@ -59,6 +64,7 @@ const environment = {
59
64
  SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
60
65
  ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY,
61
66
  SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
67
+ TOP_LEVEL_PATH: TOP_LEVEL_PATH,
62
68
  /**
63
69
  * Mock the email service in use - links to ethereal hosted emails are logged instead.
64
70
  */
package/src/index.ts CHANGED
@@ -85,10 +85,12 @@ app.use(api.routes())
85
85
 
86
86
  const server = http.createServer(app.callback())
87
87
 
88
- const shutdown = async () => {
89
- console.log("Worker service shutting down gracefully...")
88
+ const shutdown = async (signal?: string) => {
89
+ console.log(
90
+ `Worker service shutting down gracefully... ${signal ? `Signal: ${signal}` : ""}`
91
+ )
90
92
  timers.cleanup()
91
- events.shutdown()
93
+ await events.shutdown()
92
94
  await redis.clients.shutdown()
93
95
  await queue.shutdown()
94
96
  }
@@ -97,7 +99,7 @@ gracefulShutdown(server, {
97
99
  signals: "SIGINT SIGTERM",
98
100
  timeout: 30000,
99
101
  onShutdown: shutdown,
100
- forceExit: !env.isTest,
102
+ forceExit: !env.isTest(),
101
103
  finally: () => {
102
104
  console.log("Worker service shutdown complete")
103
105
  },
@@ -106,7 +108,7 @@ gracefulShutdown(server, {
106
108
  process.on("uncaughtException", async err => {
107
109
  logging.logAlert("Uncaught exception.", err)
108
110
  await shutdown()
109
- if (!env.isTest) {
111
+ if (!env.isTest()) {
110
112
  process.exit(1)
111
113
  }
112
114
  })
@@ -114,7 +116,7 @@ process.on("uncaughtException", async err => {
114
116
  process.on("unhandledRejection", async reason => {
115
117
  logging.logAlert("Unhandled Promise Rejection", reason as Error)
116
118
  await shutdown()
117
- if (!env.isTest) {
119
+ if (!env.isTest()) {
118
120
  process.exit(1)
119
121
  }
120
122
  })
@@ -37,15 +37,20 @@ import {
37
37
  } from "@budibase/types"
38
38
  import API from "./api"
39
39
  import jwt, { Secret } from "jsonwebtoken"
40
+ import http from "http"
40
41
 
41
42
  class TestConfiguration {
42
- server: any
43
- request: any
43
+ server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> =
44
+ undefined!
45
+
46
+ request: supertest.SuperTest<supertest.Test> = undefined!
47
+
44
48
  api: API
45
49
  tenantId: string
46
50
  user?: User
47
51
  apiKey?: string
48
52
  userPassword = "password123!"
53
+ sessions: string[] = []
49
54
 
50
55
  constructor(opts: { openServer: boolean } = { openServer: true }) {
51
56
  // default to cloud hosting
@@ -185,12 +190,19 @@ class TestConfiguration {
185
190
  })
186
191
  }
187
192
 
193
+ hasSession(user: User) {
194
+ return this.sessions.includes(user._id!)
195
+ }
196
+
188
197
  async createSession(user: User) {
189
- return this._createSession({
190
- userId: user._id!,
191
- tenantId: user.tenantId,
192
- email: user.email,
193
- })
198
+ if (!this.hasSession(user)) {
199
+ this.sessions.push(user._id!)
200
+ return this._createSession({
201
+ userId: user._id!,
202
+ tenantId: user.tenantId,
203
+ email: user.email,
204
+ })
205
+ }
194
206
  }
195
207
 
196
208
  cookieHeader(cookies: any) {
@@ -212,6 +224,11 @@ class TestConfiguration {
212
224
  }
213
225
  }
214
226
 
227
+ async login(user: User) {
228
+ await this.createSession(user)
229
+ return this.authHeaders(user)
230
+ }
231
+
215
232
  authHeaders(user: User) {
216
233
  const authToken: AuthToken = {
217
234
  userId: user._id!,
@@ -279,10 +296,10 @@ class TestConfiguration {
279
296
  })
280
297
  }
281
298
 
282
- async createUser(opts?: Partial<User>) {
299
+ async createUser(userCfg?: Partial<User>) {
283
300
  let user = structures.users.user()
284
301
  if (user) {
285
- user = { ...user, ...opts }
302
+ user = { ...user, ...userCfg }
286
303
  }
287
304
  const response = await this._req(user, null, controllers.users.save)
288
305
  const body = response as SaveUserResponse
@@ -1,5 +1,6 @@
1
1
  import {
2
- AIConfig,
2
+ ConfigType,
3
+ ConfigTypeToConfig,
3
4
  SaveConfigRequest,
4
5
  SaveConfigResponse,
5
6
  } from "@budibase/types"
@@ -23,12 +24,16 @@ export class ConfigAPI extends TestAPI {
23
24
  }
24
25
 
25
26
  getAIConfig = async () => {
27
+ return await this.getConfig(ConfigType.AI)
28
+ }
29
+
30
+ getConfig = async <T extends ConfigType>(type: T) => {
26
31
  const resp = await this.request
27
- .get(`/api/global/configs/ai`)
32
+ .get(`/api/global/configs/${type}`)
28
33
  .set(this.config.defaultHeaders())
29
34
  .expect(200)
30
35
  .expect("Content-Type", /json/)
31
- return resp.body as AIConfig
36
+ return resp.body as ConfigTypeToConfig<T>
32
37
  }
33
38
 
34
39
  saveConfig = async (
@@ -152,14 +152,20 @@ export class UserAPI extends TestAPI {
152
152
 
153
153
  searchUsers = (
154
154
  { query }: { query?: SearchFilters },
155
- opts?: { status?: number; noHeaders?: boolean }
155
+ opts?: {
156
+ status?: number
157
+ noHeaders?: boolean
158
+ useHeaders?: Record<string, string>
159
+ }
156
160
  ) => {
157
161
  const req = this.request
158
162
  .post("/api/global/users/search")
159
163
  .send({ query })
160
164
  .expect("Content-Type", /json/)
161
165
  .expect(opts?.status ? opts.status : 200)
162
- if (!opts?.noHeaders) {
166
+ if (opts?.useHeaders) {
167
+ req.set(opts.useHeaders)
168
+ } else if (!opts?.noHeaders) {
163
169
  req.set(this.config.defaultHeaders())
164
170
  }
165
171
  return req
@@ -5,9 +5,13 @@ import {
5
5
  SMTPConfig,
6
6
  GoogleConfig,
7
7
  OIDCConfig,
8
+ GoogleInnerConfig,
9
+ OIDCInnerConfig,
10
+ SMTPInnerConfig,
11
+ SettingsInnerConfig,
8
12
  } from "@budibase/types"
9
13
 
10
- export function oidc(conf?: any): OIDCConfig {
14
+ export function oidc(conf?: Partial<OIDCInnerConfig>): OIDCConfig {
11
15
  return {
12
16
  type: ConfigType.OIDC,
13
17
  config: {
@@ -20,6 +24,7 @@ export function oidc(conf?: any): OIDCConfig {
20
24
  name: "Active Directory",
21
25
  uuid: utils.newid(),
22
26
  activated: true,
27
+ scopes: [],
23
28
  ...conf,
24
29
  },
25
30
  ],
@@ -27,7 +32,7 @@ export function oidc(conf?: any): OIDCConfig {
27
32
  }
28
33
  }
29
34
 
30
- export function google(conf?: any): GoogleConfig {
35
+ export function google(conf?: Partial<GoogleInnerConfig>): GoogleConfig {
31
36
  return {
32
37
  type: ConfigType.GOOGLE,
33
38
  config: {
@@ -39,7 +44,7 @@ export function google(conf?: any): GoogleConfig {
39
44
  }
40
45
  }
41
46
 
42
- export function smtp(conf?: any): SMTPConfig {
47
+ export function smtp(conf?: Partial<SMTPInnerConfig>): SMTPConfig {
43
48
  return {
44
49
  type: ConfigType.SMTP,
45
50
  config: {
@@ -70,7 +75,7 @@ export function smtpEthereal(): SMTPConfig {
70
75
  }
71
76
  }
72
77
 
73
- export function settings(conf?: any): SettingsConfig {
78
+ export function settings(conf?: Partial<SettingsInnerConfig>): SettingsConfig {
74
79
  return {
75
80
  type: ConfigType.SETTINGS,
76
81
  config: {
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- const updateDotEnv = require("update-dotenv")
3
-
4
- const arg = process.argv.slice(2)[0]
5
-
6
- updateDotEnv({
7
- DISABLE_ACCOUNT_PORTAL: arg === "enable" ? "" : "1",
8
- }).then(() => console.log("Updated worker!"))
@@ -1,54 +0,0 @@
1
- #!/usr/bin/env node
2
- const { parsed: existingConfig } = require("dotenv").config()
3
- const updateDotEnv = require("update-dotenv")
4
-
5
- async function init() {
6
- let config = {
7
- SELF_HOSTED: "1",
8
- PORT: "4002",
9
- CLUSTER_PORT: "10000",
10
- JWT_SECRET: "testsecret",
11
- INTERNAL_API_KEY: "budibase",
12
- MINIO_ACCESS_KEY: "budibase",
13
- MINIO_SECRET_KEY: "budibase",
14
- REDIS_URL: "localhost:6379",
15
- REDIS_PASSWORD: "budibase",
16
- MINIO_URL: "http://localhost:4004",
17
- COUCH_DB_URL: "http://budibase:budibase@localhost:4005",
18
- COUCH_DB_USERNAME: "budibase",
19
- COUCH_DB_PASSWORD: "budibase",
20
- // empty string is false
21
- MULTI_TENANCY: "",
22
- DISABLE_ACCOUNT_PORTAL: "1",
23
- ACCOUNT_PORTAL_URL: "http://localhost:10001",
24
- PLATFORM_URL: "http://localhost:10000",
25
- APPS_URL: "http://localhost:4001",
26
- SERVICE: "worker-service",
27
- DEPLOYMENT_ENVIRONMENT: "development",
28
- ENABLE_EMAIL_TEST_MODE: "1",
29
- HTTP_LOGGING: "0",
30
- VERSION: "0.0.0+local",
31
- PASSWORD_MIN_LENGTH: "1",
32
- }
33
-
34
- config = { ...config, ...existingConfig }
35
-
36
- await updateDotEnv(config)
37
- }
38
-
39
- // if more than init required use this to determine the command type
40
- //const managementCommand = process.argv.slice(2)[0]
41
-
42
- // for now only one command
43
- let command = init
44
-
45
- command()
46
- .then(() => {
47
- console.log("Done! 🎉")
48
- })
49
- .catch(err => {
50
- console.error(
51
- "Something went wrong while managing budibase dev worker:",
52
- err.message
53
- )
54
- })
@@ -1,52 +0,0 @@
1
- #!/usr/bin/env node
2
- const updateDotEnv = require("update-dotenv")
3
-
4
- const arg = process.argv.slice(2)[0]
5
- const isEnable = arg === "enable"
6
-
7
- let domain = process.argv.slice(2)[1]
8
- if (!domain) {
9
- domain = "local.com"
10
- }
11
-
12
- const getAccountPortalUrl = () => {
13
- if (isEnable) {
14
- return `http://account.${domain}:10001`
15
- } else {
16
- return `http://localhost:10001`
17
- }
18
- }
19
-
20
- const getBudibaseUrl = () => {
21
- if (isEnable) {
22
- return `http://${domain}:10000`
23
- } else {
24
- return `http://localhost:10000`
25
- }
26
- }
27
-
28
- const getCookieDomain = () => {
29
- if (isEnable) {
30
- return `.${domain}`
31
- } else {
32
- return ""
33
- }
34
- }
35
-
36
- /**
37
- * For testing multi tenancy sub domains locally.
38
- *
39
- * Relies on an entry in /etc/hosts e.g:
40
- *
41
- * 127.0.0.1 local.com
42
- *
43
- * and an entry for each tenant you wish to test locally e.g:
44
- *
45
- * 127.0.0.1 t1.local.com
46
- * 127.0.0.1 t2.local.com
47
- */
48
- updateDotEnv({
49
- ACCOUNT_PORTAL_URL: getAccountPortalUrl(),
50
- COOKIE_DOMAIN: getCookieDomain(),
51
- PLATFORM_URL: getBudibaseUrl(),
52
- }).then(() => console.log("Updated worker!"))
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- const updateDotEnv = require("update-dotenv")
3
-
4
- const arg = process.argv.slice(2)[0]
5
-
6
- updateDotEnv({
7
- MULTI_TENANCY: arg === "enable" ? "1" : "",
8
- }).then(() => console.log("Updated worker!"))
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- const updateDotEnv = require("update-dotenv")
3
-
4
- const arg = process.argv.slice(2)[0]
5
-
6
- updateDotEnv({
7
- SELF_HOSTED: arg === "enable" ? "1" : "0",
8
- }).then(() => console.log("Updated worker!"))