@budibase/worker 3.35.3 → 3.35.10

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.
@@ -1,6 +1,10 @@
1
1
  const actual = jest.requireActual("@budibase/pro")
2
2
  const pro = {
3
3
  ...actual,
4
+ scimUsers: {
5
+ ...actual.scimUsers,
6
+ handleDisable: jest.fn(),
7
+ },
4
8
  features: {
5
9
  ...actual.features,
6
10
  isSSOEnforced: jest.fn(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "3.35.3",
4
+ "version": "3.35.10",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -15,7 +15,7 @@
15
15
  "prebuild": "rimraf dist/",
16
16
  "build": "node ./scripts/build.js",
17
17
  "postbuild": "copyfiles -f ../../yarn.lock ./dist/",
18
- "check:types": "tsc -p tsconfig.json --noEmit --paths null",
18
+ "check:types": "tsc -p tsconfig.json --noEmit --incremental --paths null",
19
19
  "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
20
20
  "run:docker": "node --enable-source-maps dist/index.js",
21
21
  "debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js",
@@ -55,7 +55,7 @@
55
55
  "lodash": "4.18.1",
56
56
  "marked": "^15.0.11",
57
57
  "node-fetch": "2.6.7",
58
- "nodemailer": "8.0.4",
58
+ "nodemailer": "8.0.5",
59
59
  "pouchdb": "9.0.0",
60
60
  "scim-patch": "^0.8.1",
61
61
  "scim2-parse-filter": "^0.2.8",
@@ -79,5 +79,5 @@
79
79
  "supertest": "6.3.3",
80
80
  "timekeeper": "2.2.0"
81
81
  },
82
- "gitHead": "d5b05b266b9b7726bdcd7a9e7affb3330129805c"
82
+ "gitHead": "e2fa586beb21f730b22350e561d21ec1e2c5c2f0"
83
83
  }
@@ -95,7 +95,10 @@ async function passportCallback(
95
95
  const loginResult = await authSdk.loginUser(user)
96
96
 
97
97
  // set a cookie for browser access
98
- setCookie(ctx, loginResult.token, Cookie.Auth, { sign: false })
98
+ setCookie(ctx, loginResult.token, Cookie.Auth, {
99
+ sign: false,
100
+ httpOnly: true,
101
+ })
99
102
  // set the token in a header as well for APIs
100
103
  ctx.set(Header.TOKEN, loginResult.token)
101
104
 
@@ -282,14 +285,20 @@ export const datasourcePreAuth = async (
282
285
  next: Next
283
286
  ) => {
284
287
  const provider = ctx.params.provider
288
+ const returnPath =
289
+ typeof ctx.query.returnPath === "string" ? ctx.query.returnPath : undefined
285
290
  const { middleware } = require(`@budibase/backend-core`)
286
291
  const handler = middleware.datasource[provider]
292
+ if (!handler) {
293
+ ctx.throw(400, "Unsupported datasource provider")
294
+ }
287
295
 
288
296
  setCookie(
289
297
  ctx,
290
298
  {
291
299
  provider,
292
300
  appId: ctx.query.appId,
301
+ returnPath,
293
302
  },
294
303
  Cookie.DatasourceAuth
295
304
  )
@@ -308,6 +317,9 @@ export const datasourceAuth = async (ctx: UserCtx<void, void>, next: Next) => {
308
317
  const provider = authStateCookie.provider
309
318
  const { middleware } = require(`@budibase/backend-core`)
310
319
  const handler = middleware.datasource[provider]
320
+ if (!handler) {
321
+ ctx.throw(400, "Unsupported datasource provider")
322
+ }
311
323
  return handler.postAuth(passport, ctx, next)
312
324
  }
313
325
 
@@ -36,6 +36,8 @@ import {
36
36
  OIDCLogosConfig,
37
37
  PASSWORD_REPLACEMENT,
38
38
  RecaptchaInnerConfig,
39
+ SCIMDisableAction,
40
+ SCIMInnerConfig,
39
41
  SaveConfigRequest,
40
42
  SaveConfigResponse,
41
43
  SettingsBrandingConfig,
@@ -346,12 +348,26 @@ async function processTranslationsConfig(
346
348
  ctx.request.body.config = prepareTranslationsConfig(ctx, config)
347
349
  }
348
350
 
351
+ async function processSCIMConfig(
352
+ newConfig: SCIMInnerConfig,
353
+ existingConfig: SCIMInnerConfig | undefined
354
+ ): Promise<SCIMDisableAction | undefined> {
355
+ const { disableAction } = newConfig
356
+ delete newConfig.disableAction
357
+
358
+ const isBeingDisabled = existingConfig?.enabled && !newConfig.enabled
359
+ if (isBeingDisabled && disableAction) {
360
+ return disableAction
361
+ }
362
+ }
363
+
349
364
  export async function save(
350
365
  ctx: UserCtx<SaveConfigRequest, SaveConfigResponse>
351
366
  ) {
352
367
  const body = ctx.request.body
353
368
  const type = body.type
354
369
  const config = body.config
370
+ let scimDisableAction: SCIMDisableAction | undefined
355
371
 
356
372
  const existingConfig = await configs.getConfig(type)
357
373
  let eventFns = await getEventFns(ctx.request.body, existingConfig)
@@ -380,6 +396,12 @@ export async function save(
380
396
  case ConfigType.TRANSLATIONS:
381
397
  await processTranslationsConfig(ctx, config)
382
398
  break
399
+ case ConfigType.SCIM:
400
+ scimDisableAction = await processSCIMConfig(
401
+ config,
402
+ existingConfig?.config
403
+ )
404
+ break
383
405
  }
384
406
  } catch (err: any) {
385
407
  ctx.throw(400, err?.message || err)
@@ -425,6 +447,19 @@ export async function save(
425
447
  await fn()
426
448
  }
427
449
 
450
+ if (scimDisableAction) {
451
+ const tenantId = tenancy.getTenantId()
452
+ setImmediate(async () => {
453
+ try {
454
+ await tenancy.doInTenant(tenantId, () =>
455
+ pro.scimUsers.handleDisable(scimDisableAction!)
456
+ )
457
+ } catch (e) {
458
+ console.error("Error processing SCIM users on disable:", e)
459
+ }
460
+ })
461
+ }
462
+
428
463
  ctx.body = {
429
464
  type,
430
465
  _id: response.id,
@@ -107,7 +107,7 @@ export const update = async (ctx: Ctx<ScimUpdateRequest, ScimUserResponse>) => {
107
107
  ctx.throw(500)
108
108
  }
109
109
 
110
- const userToUpdate = mappers.user.fromScimUser(patchedScimUser)
110
+ const userToUpdate = mappers.user.fromScimUser(patchedScimUser, user.roles)
111
111
  await scimUsers.update(userToUpdate, { allowChangingEmail: true })
112
112
 
113
113
  ctx.body = mappers.user.toScimUserResponse(userToUpdate)
@@ -5,6 +5,7 @@ import {
5
5
  ConfigType,
6
6
  GetPublicSettingsResponse,
7
7
  PKCEMethod,
8
+ SCIMConfig,
8
9
  TranslationsConfig,
9
10
  } from "@budibase/types"
10
11
  import { TestConfiguration, mocks, structures } from "../../../../tests"
@@ -402,6 +403,71 @@ describe("configs", () => {
402
403
  })
403
404
  })
404
405
 
406
+ describe("scim", () => {
407
+ const scimConfig = (enabled: boolean, disableAction?: string): SCIMConfig =>
408
+ ({
409
+ type: ConfigType.SCIM,
410
+ config: { enabled, ...(disableAction ? { disableAction } : {}) },
411
+ }) as SCIMConfig
412
+
413
+ beforeEach(async () => {
414
+ await config.deleteConfig(ConfigType.SCIM)
415
+ jest.clearAllMocks()
416
+ mocks.pro.scimUsers.handleDisable.mockResolvedValue(undefined)
417
+ })
418
+
419
+ afterEach(async () => {
420
+ await config.deleteConfig(ConfigType.SCIM)
421
+ })
422
+
423
+ it("calls handleDisable with 'remove' when SCIM is disabled with remove action", async () => {
424
+ await config.api.configs.saveConfig(scimConfig(true))
425
+ jest.clearAllMocks()
426
+
427
+ await config.api.configs.saveConfig(scimConfig(false, "remove"))
428
+ await new Promise<void>(resolve => setImmediate(resolve))
429
+
430
+ expect(mocks.pro.scimUsers.handleDisable).toHaveBeenCalledTimes(1)
431
+ expect(mocks.pro.scimUsers.handleDisable).toHaveBeenCalledWith("remove")
432
+ })
433
+
434
+ it("calls handleDisable with 'convert' when SCIM is disabled with convert action", async () => {
435
+ await config.api.configs.saveConfig(scimConfig(true))
436
+ jest.clearAllMocks()
437
+
438
+ await config.api.configs.saveConfig(scimConfig(false, "convert"))
439
+ await new Promise<void>(resolve => setImmediate(resolve))
440
+
441
+ expect(mocks.pro.scimUsers.handleDisable).toHaveBeenCalledTimes(1)
442
+ expect(mocks.pro.scimUsers.handleDisable).toHaveBeenCalledWith("convert")
443
+ })
444
+
445
+ it("does not call handleDisable when SCIM is disabled without a disableAction", async () => {
446
+ await config.api.configs.saveConfig(scimConfig(true))
447
+ jest.clearAllMocks()
448
+
449
+ await config.api.configs.saveConfig(scimConfig(false))
450
+ await new Promise<void>(resolve => setImmediate(resolve))
451
+
452
+ expect(mocks.pro.scimUsers.handleDisable).not.toHaveBeenCalled()
453
+ })
454
+
455
+ it("does not call handleDisable when SCIM is being enabled", async () => {
456
+ await config.api.configs.saveConfig(scimConfig(true, "remove"))
457
+ await new Promise<void>(resolve => setImmediate(resolve))
458
+
459
+ expect(mocks.pro.scimUsers.handleDisable).not.toHaveBeenCalled()
460
+ })
461
+
462
+ it("does not persist disableAction to the saved config", async () => {
463
+ await config.api.configs.saveConfig(scimConfig(true))
464
+ await config.api.configs.saveConfig(scimConfig(false, "remove"))
465
+
466
+ const saved = await config.api.configs.getConfig(ConfigType.SCIM)
467
+ expect(saved.config).not.toHaveProperty("disableAction")
468
+ })
469
+ })
470
+
405
471
  describe("GET /api/global/configs/checklist", () => {
406
472
  it("should return the correct checklist", async () => {
407
473
  await config.saveSmtpConfig()
@@ -364,9 +364,12 @@ describe("scim", () => {
364
364
  })
365
365
 
366
366
  it("creating an external user that conflicts an internal one syncs the existing user", async () => {
367
- const { body: internalUser } = await config.api.users.saveUser(
368
- structures.users.user()
369
- )
367
+ const workspaceId = "app_scim_sync_roles"
368
+ const explicitRoles = { [workspaceId]: "BASIC" }
369
+ const { body: internalUser } = await config.api.users.saveUser({
370
+ ...structures.users.user(),
371
+ roles: explicitRoles,
372
+ })
370
373
 
371
374
  const scimUserData = {
372
375
  externalId: structures.uuid(),
@@ -410,6 +413,14 @@ describe("scim", () => {
410
413
  }
411
414
 
412
415
  expect(res).toEqual(expectedScimUser)
416
+
417
+ expect(
418
+ (await config.api.users.getUser(internalUser._id!)).body
419
+ ).toEqual(
420
+ expect.objectContaining({
421
+ roles: explicitRoles,
422
+ })
423
+ )
413
424
  })
414
425
 
415
426
  it("a user cannot be SCIM synchronised with another SCIM user", async () => {
@@ -516,6 +527,46 @@ describe("scim", () => {
516
527
  expect(persistedUser).toEqual(expectedScimUser)
517
528
  })
518
529
 
530
+ it("preserves explicit roles when updating an existing SCIM user", async () => {
531
+ const workspaceId = "app_scim_patch_roles"
532
+ const explicitRoles = { [workspaceId]: "BASIC" }
533
+ const { body: internalUser } = await config.api.users.saveUser({
534
+ ...structures.users.user(),
535
+ roles: explicitRoles,
536
+ })
537
+
538
+ const syncedUser = await config.api.scimUsersAPI.post(
539
+ {
540
+ body: structures.scim.createUserRequest({
541
+ email: internalUser.email,
542
+ }),
543
+ },
544
+ { expect: 200 }
545
+ )
546
+
547
+ const newFamilyName = structures.generator.last()
548
+ const body: ScimUpdateRequest = {
549
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
550
+ Operations: [
551
+ {
552
+ op: "Replace",
553
+ path: "name.familyName",
554
+ value: newFamilyName,
555
+ },
556
+ ],
557
+ }
558
+
559
+ await patchScimUser({ id: syncedUser.id, body })
560
+
561
+ expect((await config.api.users.getUser(syncedUser.id)).body).toEqual(
562
+ expect.objectContaining({
563
+ _id: syncedUser.id,
564
+ lastName: newFamilyName,
565
+ roles: explicitRoles,
566
+ })
567
+ )
568
+ })
569
+
519
570
  it.each([false, "false", "False"])(
520
571
  "deactivating an active user (sending %s) will delete it",
521
572
  async activeValue => {
@@ -1138,6 +1138,14 @@ describe("/api/global/users", () => {
1138
1138
  await config.api.users.searchUsers({}, { status: 403, noHeaders: true })
1139
1139
  })
1140
1140
 
1141
+ it("should throw an error if a public route is injected in the query string", async () => {
1142
+ await config.request
1143
+ .post("/api/global/users/search?x=/api/system/status")
1144
+ .send({})
1145
+ .expect("Content-Type", /json/)
1146
+ .expect(403)
1147
+ })
1148
+
1141
1149
  it("should be able to search using logical conditions", async () => {
1142
1150
  const user = await config.createUser()
1143
1151
  const response = await config.api.users.searchUsers({