@budibase/worker 2.33.2 → 2.33.4

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "2.33.2",
4
+ "version": "2.33.4",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -37,10 +37,10 @@
37
37
  "author": "Budibase",
38
38
  "license": "GPL-3.0",
39
39
  "dependencies": {
40
- "@budibase/backend-core": "2.33.2",
41
- "@budibase/pro": "2.33.2",
42
- "@budibase/string-templates": "2.33.2",
43
- "@budibase/types": "2.33.2",
40
+ "@budibase/backend-core": "2.33.4",
41
+ "@budibase/pro": "2.33.4",
42
+ "@budibase/string-templates": "2.33.4",
43
+ "@budibase/types": "2.33.4",
44
44
  "@koa/router": "8.0.8",
45
45
  "@techpass/passport-openidconnect": "0.3.3",
46
46
  "@types/global-agent": "2.1.1",
@@ -107,5 +107,5 @@
107
107
  }
108
108
  }
109
109
  },
110
- "gitHead": "72e28bac4ad58c8cf88badd8582545ba6f9aa671"
110
+ "gitHead": "3fdac98d5137c43e5c8593f24a546338bc0ec36f"
111
111
  }
@@ -44,9 +44,7 @@ const getEventFns = async (config: Config, existing?: Config) => {
44
44
  fns.push(events.email.SMTPCreated)
45
45
  } else if (isAIConfig(config)) {
46
46
  fns.push(() => events.ai.AIConfigCreated)
47
- fns.push(() =>
48
- pro.quotas.updateCustomAIConfigCount(Object.keys(config.config).length)
49
- )
47
+ fns.push(() => pro.quotas.addCustomAIConfig())
50
48
  } else if (isGoogleConfig(config)) {
51
49
  fns.push(() => events.auth.SSOCreated(ConfigType.GOOGLE))
52
50
  if (config.config.activated) {
@@ -85,9 +83,6 @@ const getEventFns = async (config: Config, existing?: Config) => {
85
83
  fns.push(events.email.SMTPUpdated)
86
84
  } else if (isAIConfig(config)) {
87
85
  fns.push(() => events.ai.AIConfigUpdated)
88
- fns.push(() =>
89
- pro.quotas.updateCustomAIConfigCount(Object.keys(config.config).length)
90
- )
91
86
  } else if (isGoogleConfig(config)) {
92
87
  fns.push(() => events.auth.SSOUpdated(ConfigType.GOOGLE))
93
88
  if (!existing.config.activated && config.config.activated) {
@@ -253,7 +248,7 @@ export async function save(ctx: UserCtx<Config>) {
253
248
  if (existingConfig) {
254
249
  await verifyAIConfig(config, existingConfig)
255
250
  }
256
- await pro.quotas.updateCustomAIConfigCount(Object.keys(config).length)
251
+ await pro.quotas.addCustomAIConfig()
257
252
  break
258
253
  }
259
254
  } catch (err: any) {
@@ -342,29 +337,43 @@ export async function find(ctx: UserCtx) {
342
337
  let scopedConfig = await configs.getConfig(type)
343
338
 
344
339
  if (scopedConfig) {
345
- if (type === ConfigType.OIDC_LOGOS) {
346
- enrichOIDCLogos(scopedConfig)
347
- }
348
-
349
- if (type === ConfigType.AI) {
350
- await pro.sdk.ai.enrichAIConfig(scopedConfig)
351
- // Strip out the API Keys from the response so they don't show in the UI
352
- for (const key in scopedConfig.config) {
353
- if (scopedConfig.config[key].apiKey) {
354
- scopedConfig.config[key].apiKey = PASSWORD_REPLACEMENT
355
- }
356
- }
357
- }
358
- ctx.body = scopedConfig
340
+ await handleConfigType(type, scopedConfig)
341
+ } else if (type === ConfigType.AI) {
342
+ scopedConfig = { config: {} } as AIConfig
343
+ await handleAIConfig(scopedConfig)
359
344
  } else {
360
- // don't throw an error, there simply is nothing to return
345
+ // If no config found and not AI type, just return an empty body
361
346
  ctx.body = {}
347
+ return
362
348
  }
349
+
350
+ ctx.body = scopedConfig
363
351
  } catch (err: any) {
364
352
  ctx.throw(err?.status || 400, err)
365
353
  }
366
354
  }
367
355
 
356
+ async function handleConfigType(type: ConfigType, config: Config) {
357
+ if (type === ConfigType.OIDC_LOGOS) {
358
+ enrichOIDCLogos(config)
359
+ } else if (type === ConfigType.AI) {
360
+ await handleAIConfig(config)
361
+ }
362
+ }
363
+
364
+ async function handleAIConfig(config: AIConfig) {
365
+ await pro.sdk.ai.enrichAIConfig(config)
366
+ stripApiKeys(config)
367
+ }
368
+
369
+ function stripApiKeys(config: AIConfig) {
370
+ for (const key in config?.config) {
371
+ if (config.config[key].apiKey) {
372
+ config.config[key].apiKey = PASSWORD_REPLACEMENT
373
+ }
374
+ }
375
+ }
376
+
368
377
  export async function publicOidc(ctx: Ctx<void, GetPublicOIDCConfigResponse>) {
369
378
  try {
370
379
  // Find the config with the most granular scope based on context
@@ -508,6 +517,9 @@ export async function destroy(ctx: UserCtx) {
508
517
  try {
509
518
  await db.remove(id, rev)
510
519
  await cache.destroy(cache.CacheKey.CHECKLIST)
520
+ if (id === configs.generateConfigID(ConfigType.AI)) {
521
+ await pro.quotas.removeCustomAIConfig()
522
+ }
511
523
  ctx.body = { message: "Config deleted successfully" }
512
524
  } catch (err: any) {
513
525
  ctx.throw(err.status, err)
@@ -13,10 +13,6 @@ describe("Global configs controller", () => {
13
13
  await config.afterAll()
14
14
  })
15
15
 
16
- afterEach(() => {
17
- jest.resetAllMocks()
18
- })
19
-
20
16
  it("Should strip secrets when pulling AI config", async () => {
21
17
  const data = structures.configs.ai()
22
18
  await config.api.configs.saveConfig(data)
@@ -23,9 +23,11 @@ import {
23
23
  SearchUsersRequest,
24
24
  User,
25
25
  UserCtx,
26
+ UserIdentifier,
26
27
  } from "@budibase/types"
27
28
  import {
28
29
  accounts,
30
+ users,
29
31
  cache,
30
32
  ErrorCode,
31
33
  events,
@@ -55,8 +57,8 @@ export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
55
57
  const requestUser = ctx.request.body
56
58
 
57
59
  // Do not allow the account holder role to be changed
58
- const tenantInfo = await tenancy.getTenantInfo(requestUser.tenantId)
59
- if (tenantInfo?.owner.email === requestUser.email) {
60
+ const accountMetadata = await users.getExistingAccounts([requestUser.email])
61
+ if (accountMetadata?.length > 0) {
60
62
  if (
61
63
  requestUser.admin?.global !== true ||
62
64
  requestUser.builder?.global !== true
@@ -103,11 +105,14 @@ export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
103
105
  }
104
106
  }
105
107
 
106
- const bulkDelete = async (userIds: string[], currentUserId: string) => {
107
- if (userIds?.indexOf(currentUserId) !== -1) {
108
+ const bulkDelete = async (
109
+ users: Array<UserIdentifier>,
110
+ currentUserId: string
111
+ ) => {
112
+ if (users.find(u => u.userId === currentUserId)) {
108
113
  throw new Error("Unable to delete self.")
109
114
  }
110
- return await userSdk.db.bulkDelete(userIds)
115
+ return await userSdk.db.bulkDelete(users)
111
116
  }
112
117
 
113
118
  const bulkCreate = async (users: User[], groupIds: string[]) => {
@@ -130,7 +135,7 @@ export const bulkUpdate = async (
130
135
  created = await bulkCreate(input.create.users, input.create.groups)
131
136
  }
132
137
  if (input.delete) {
133
- deleted = await bulkDelete(input.delete.userIds, currentUserId)
138
+ deleted = await bulkDelete(input.delete.users, currentUserId)
134
139
  }
135
140
  } catch (err: any) {
136
141
  ctx.throw(err.status || 400, err?.message || err)
@@ -302,6 +307,23 @@ export const tenantUserLookup = async (ctx: any) => {
302
307
  }
303
308
  }
304
309
 
310
+ /**
311
+ * This will be paginated to a default of the first 50 users,
312
+ * So the account holder may not be found until further pagination has occurred
313
+ */
314
+ export const accountHolderLookup = async (ctx: Ctx) => {
315
+ const users = await userSdk.core.getAllUsers()
316
+ const response = await userSdk.core.getExistingAccounts(
317
+ users.map(u => u.email)
318
+ )
319
+ const holder = response[0]
320
+ if (!holder) {
321
+ return
322
+ }
323
+ holder._id = users.find(u => u.email === holder.email)?._id
324
+ ctx.body = holder
325
+ }
326
+
305
327
  /*
306
328
  Encapsulate the app user onboarding flows here.
307
329
  */
@@ -1,4 +1,4 @@
1
- import { Ctx, MaintenanceType } from "@budibase/types"
1
+ import { Ctx, MaintenanceType, FeatureFlag } from "@budibase/types"
2
2
  import env from "../../../environment"
3
3
  import { env as coreEnv, db as dbCore, features } from "@budibase/backend-core"
4
4
  import nodeFetch from "node-fetch"
@@ -29,7 +29,10 @@ async function isSqsAvailable() {
29
29
  }
30
30
 
31
31
  async function isSqsMissing() {
32
- return (await features.flags.isEnabled("SQS")) && !(await isSqsAvailable())
32
+ return (
33
+ (await features.flags.isEnabled(FeatureFlag.SQS)) &&
34
+ !(await isSqsAvailable())
35
+ )
33
36
  }
34
37
 
35
38
  export const fetch = async (ctx: Ctx) => {
package/src/api/index.ts CHANGED
@@ -71,10 +71,6 @@ const PUBLIC_ENDPOINTS = [
71
71
  route: "/api/global/users/invite",
72
72
  method: "GET",
73
73
  },
74
- {
75
- route: "/api/global/tenant",
76
- method: "POST",
77
- },
78
74
  ]
79
75
 
80
76
  const NO_TENANCY_ENDPOINTS = [
@@ -121,11 +117,7 @@ const NO_TENANCY_ENDPOINTS = [
121
117
  method: "GET",
122
118
  },
123
119
  {
124
- route: "/api/global/tenant",
125
- method: "POST",
126
- },
127
- {
128
- route: "/api/global/tenant/:id",
120
+ route: "/api/global/users/accountholder",
129
121
  method: "GET",
130
122
  },
131
123
  ]
@@ -1,6 +1,6 @@
1
1
  import { structures, TestConfiguration } from "../../../../tests"
2
2
  import { context, db, permissions, roles } from "@budibase/backend-core"
3
- import { Database } from "@budibase/types"
3
+ import { App, Database } from "@budibase/types"
4
4
 
5
5
  jest.mock("@budibase/backend-core", () => {
6
6
  const core = jest.requireActual("@budibase/backend-core")
@@ -30,6 +30,14 @@ async function addAppMetadata() {
30
30
  })
31
31
  }
32
32
 
33
+ async function updateAppMetadata(update: Partial<Omit<App, "_id" | "_rev">>) {
34
+ const app = await appDb.get("app_metadata")
35
+ await appDb.put({
36
+ ...app,
37
+ ...update,
38
+ })
39
+ }
40
+
33
41
  describe("/api/global/roles", () => {
34
42
  const config = new TestConfiguration()
35
43
 
@@ -69,6 +77,53 @@ describe("/api/global/roles", () => {
69
77
  expect(res.body[appId].roles.length).toEqual(5)
70
78
  expect(res.body[appId].roles.map((r: any) => r._id)).toContain(ROLE_NAME)
71
79
  })
80
+
81
+ it.each(["3.0.0", "3.0.1", "3.1.0", "3.0.0+2146.b125a7c"])(
82
+ "exclude POWER roles after v3 (%s)",
83
+ async creationVersion => {
84
+ await updateAppMetadata({ creationVersion })
85
+ const res = await config.api.roles.get()
86
+ expect(res.body).toBeDefined()
87
+ expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
88
+ ROLE_NAME,
89
+ roles.BUILTIN_ROLE_IDS.ADMIN,
90
+ roles.BUILTIN_ROLE_IDS.BASIC,
91
+ roles.BUILTIN_ROLE_IDS.PUBLIC,
92
+ ])
93
+ }
94
+ )
95
+
96
+ it.each(["2.9.0", "1.0.0", "0.0.0", "2.32.17+2146.b125a7c"])(
97
+ "include POWER roles before v3 (%s)",
98
+ async creationVersion => {
99
+ await updateAppMetadata({ creationVersion })
100
+ const res = await config.api.roles.get()
101
+ expect(res.body).toBeDefined()
102
+ expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
103
+ ROLE_NAME,
104
+ roles.BUILTIN_ROLE_IDS.ADMIN,
105
+ roles.BUILTIN_ROLE_IDS.POWER,
106
+ roles.BUILTIN_ROLE_IDS.BASIC,
107
+ roles.BUILTIN_ROLE_IDS.PUBLIC,
108
+ ])
109
+ }
110
+ )
111
+
112
+ it.each(["invalid", ""])(
113
+ "include POWER roles when the version is corrupted (%s)",
114
+ async creationVersion => {
115
+ await updateAppMetadata({ creationVersion })
116
+ const res = await config.api.roles.get()
117
+
118
+ expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
119
+ ROLE_NAME,
120
+ roles.BUILTIN_ROLE_IDS.ADMIN,
121
+ roles.BUILTIN_ROLE_IDS.POWER,
122
+ roles.BUILTIN_ROLE_IDS.BASIC,
123
+ roles.BUILTIN_ROLE_IDS.PUBLIC,
124
+ ])
125
+ }
126
+ )
72
127
  })
73
128
 
74
129
  describe("GET api/global/roles/:appId", () => {
@@ -412,28 +412,6 @@ describe("/api/global/users", () => {
412
412
  expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1)
413
413
  })
414
414
 
415
- it("should not be able to update an account holder user to a basic user", async () => {
416
- const accountHolderUser = await config.createUser(
417
- structures.users.adminUser()
418
- )
419
- jest.clearAllMocks()
420
- tenancy.getTenantInfo = jest.fn().mockImplementation(() => ({
421
- owner: {
422
- email: accountHolderUser.email,
423
- },
424
- }))
425
-
426
- accountHolderUser.admin!.global = false
427
- accountHolderUser.builder!.global = false
428
-
429
- await config.api.users.saveUser(accountHolderUser, 400)
430
-
431
- expect(events.user.created).not.toHaveBeenCalled()
432
- expect(events.user.updated).not.toHaveBeenCalled()
433
- expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled()
434
- expect(events.user.permissionBuilderRemoved).not.toHaveBeenCalled()
435
- })
436
-
437
415
  it("should be able to update an builder user to a basic user", async () => {
438
416
  const user = await config.createUser(structures.users.builderUser())
439
417
  jest.clearAllMocks()
@@ -592,55 +570,21 @@ describe("/api/global/users", () => {
592
570
 
593
571
  describe("POST /api/global/users/bulk (delete)", () => {
594
572
  it("should not be able to bulk delete current user", async () => {
595
- const user = await config.user!
573
+ const user = config.user!
596
574
 
597
- const response = await config.api.users.bulkDeleteUsers([user._id!], 400)
575
+ const response = await config.api.users.bulkDeleteUsers(
576
+ [
577
+ {
578
+ userId: user._id!,
579
+ email: "test@example.com",
580
+ },
581
+ ],
582
+ 400
583
+ )
598
584
 
599
585
  expect(response.message).toBe("Unable to delete self.")
600
586
  expect(events.user.deleted).not.toHaveBeenCalled()
601
587
  })
602
-
603
- it("should not be able to bulk delete account owner", async () => {
604
- const user = await config.createUser()
605
- const account = structures.accounts.cloudAccount()
606
- account.budibaseUserId = user._id!
607
- accounts.getAccountByTenantId.mockReturnValue(Promise.resolve(account))
608
-
609
- const response = await config.api.users.bulkDeleteUsers([user._id!])
610
-
611
- expect(response.deleted?.successful.length).toBe(0)
612
- expect(response.deleted?.unsuccessful.length).toBe(1)
613
- expect(response.deleted?.unsuccessful[0].reason).toBe(
614
- "Account holder cannot be deleted"
615
- )
616
- expect(response.deleted?.unsuccessful[0]._id).toBe(user._id)
617
- expect(events.user.deleted).not.toHaveBeenCalled()
618
- })
619
-
620
- it("should be able to bulk delete users", async () => {
621
- const account = structures.accounts.cloudAccount()
622
- accounts.getAccountByTenantId.mockReturnValue(Promise.resolve(account))
623
-
624
- const builder = structures.users.builderUser()
625
- const admin = structures.users.adminUser()
626
- const user = structures.users.user()
627
- const createdUsers = await config.api.users.bulkCreateUsers([
628
- builder,
629
- admin,
630
- user,
631
- ])
632
-
633
- const toDelete = createdUsers.created?.successful.map(
634
- u => u._id!
635
- ) as string[]
636
- const response = await config.api.users.bulkDeleteUsers(toDelete)
637
-
638
- expect(response.deleted?.successful.length).toBe(3)
639
- expect(response.deleted?.unsuccessful.length).toBe(0)
640
- expect(events.user.deleted).toHaveBeenCalledTimes(3)
641
- expect(events.user.permissionAdminRemoved).toHaveBeenCalledTimes(1)
642
- expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(2)
643
- })
644
588
  })
645
589
 
646
590
  describe("POST /api/global/users/search", () => {
@@ -136,6 +136,7 @@ router
136
136
  buildAdminInitValidation(),
137
137
  controller.adminUser
138
138
  )
139
+ .get("/api/global/users/accountholder", controller.accountHolderLookup)
139
140
  .get("/api/global/users/tenant/:id", controller.tenantUserLookup)
140
141
  // global endpoint but needs to come at end (blocks other endpoints otherwise)
141
142
  .get("/api/global/users/:id", auth.builderOrAdmin, controller.find)
@@ -1,7 +1,6 @@
1
1
  import Router from "@koa/router"
2
2
  import { api as pro } from "@budibase/pro"
3
3
  import userRoutes from "./global/users"
4
- import tenantRoutes from "./global/tenant"
5
4
  import configRoutes from "./global/configs"
6
5
  import workspaceRoutes from "./global/workspaces"
7
6
  import templateRoutes from "./global/templates"
@@ -41,7 +40,6 @@ export const routes: Router[] = [
41
40
  accountRoutes,
42
41
  restoreRoutes,
43
42
  eventRoutes,
44
- tenantRoutes,
45
43
  pro.scim,
46
44
  ]
47
45
 
@@ -66,7 +66,14 @@ export const buildUserBulkUserValidation = (isSelf = false) => {
66
66
  users: Joi.array().items(Joi.object(schema).required().unknown(true)),
67
67
  }),
68
68
  delete: Joi.object({
69
- userIds: Joi.array().items(Joi.string()),
69
+ users: Joi.array().items(
70
+ Joi.object({
71
+ email: Joi.string(),
72
+ userId: Joi.string(),
73
+ })
74
+ .required()
75
+ .unknown(true)
76
+ ),
70
77
  }),
71
78
  }
72
79
 
@@ -1,4 +1,3 @@
1
- import { TenantInfo } from "@budibase/types"
2
1
  import TestConfiguration from "../TestConfiguration"
3
2
  import { TestAPI, TestAPIOpts } from "./base"
4
3
 
@@ -15,12 +14,4 @@ export class TenantAPI extends TestAPI {
15
14
  .set(opts?.headers)
16
15
  .expect(opts?.status ? opts.status : 204)
17
16
  }
18
-
19
- saveTenantInfo = (tenantInfo: TenantInfo) => {
20
- return this.request
21
- .post("/api/global/tenant")
22
- .set(this.config.internalAPIHeaders())
23
- .send(tenantInfo)
24
- .expect(200)
25
- }
26
17
  }
@@ -81,8 +81,14 @@ export class UserAPI extends TestAPI {
81
81
  return res.body as BulkUserResponse
82
82
  }
83
83
 
84
- bulkDeleteUsers = async (userIds: string[], status?: number) => {
85
- const body: BulkUserRequest = { delete: { userIds } }
84
+ bulkDeleteUsers = async (
85
+ users: Array<{
86
+ userId: string
87
+ email: string
88
+ }>,
89
+ status?: number
90
+ ) => {
91
+ const body: BulkUserRequest = { delete: { users } }
86
92
  const res = await this.request
87
93
  .post(`/api/global/users/bulk`)
88
94
  .send(body)
@@ -1,14 +0,0 @@
1
- import { tenancy } from "@budibase/backend-core"
2
- import { TenantInfo, Ctx } from "@budibase/types"
3
-
4
- export const save = async (ctx: Ctx<TenantInfo>) => {
5
- const response = await tenancy.saveTenantInfo(ctx.request.body)
6
- ctx.body = {
7
- _id: response.id,
8
- _rev: response.rev,
9
- }
10
- }
11
-
12
- export const get = async (ctx: Ctx) => {
13
- ctx.body = await tenancy.getTenantInfo(ctx.params.id)
14
- }
@@ -1,11 +0,0 @@
1
- import Router from "@koa/router"
2
- import * as controller from "../../controllers/global/tenant"
3
- import cloudRestricted from "../../../middleware/cloudRestricted"
4
-
5
- const router: Router = new Router()
6
-
7
- router
8
- .post("/api/global/tenant", cloudRestricted, controller.save)
9
- .get("/api/global/tenant/:id", controller.get)
10
-
11
- export default router
@@ -1,48 +0,0 @@
1
- import { Hosting, TenantInfo } from "@budibase/types"
2
- import { TestConfiguration } from "../../../../tests"
3
- import { tenancy as _tenancy } from "@budibase/backend-core"
4
-
5
- const tenancy = jest.mocked(_tenancy)
6
-
7
- describe("/api/global/tenant", () => {
8
- const config = new TestConfiguration()
9
-
10
- beforeAll(async () => {
11
- await config.beforeAll()
12
- })
13
-
14
- afterAll(async () => {
15
- await config.afterAll()
16
- })
17
-
18
- beforeEach(() => {
19
- jest.clearAllMocks()
20
- })
21
-
22
- describe("POST /api/global/tenant", () => {
23
- it("should save the tenantInfo", async () => {
24
- tenancy.saveTenantInfo = jest.fn().mockImplementation(async () => ({
25
- id: "DOC_ID",
26
- ok: true,
27
- rev: "DOC_REV",
28
- }))
29
- const tenantInfo: TenantInfo = {
30
- owner: {
31
- email: "test@example.com",
32
- password: "PASSWORD123!",
33
- ssoId: "SSO_ID",
34
- givenName: "Jane",
35
- familyName: "Doe",
36
- budibaseUserId: "USER_ID",
37
- },
38
- tenantId: "tenant123",
39
- hosting: Hosting.CLOUD,
40
- }
41
- const response = await config.api.tenants.saveTenantInfo(tenantInfo)
42
-
43
- expect(_tenancy.saveTenantInfo).toHaveBeenCalledTimes(1)
44
- expect(_tenancy.saveTenantInfo).toHaveBeenCalledWith(tenantInfo)
45
- expect(response.text).toEqual('{"_id":"DOC_ID","_rev":"DOC_REV"}')
46
- })
47
- })
48
- })