@budibase/worker 1.2.58 → 1.3.1

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.
Files changed (42) hide show
  1. package/package.json +7 -6
  2. package/scripts/jestSetup.js +6 -0
  3. package/src/api/controllers/global/auth.ts +14 -1
  4. package/src/api/controllers/global/users.ts +26 -55
  5. package/src/api/controllers/system/accounts.ts +21 -0
  6. package/src/api/{index.js → index.ts} +12 -16
  7. package/src/api/routes/global/configs.js +1 -0
  8. package/src/api/routes/{tests/auth.spec.js → global/tests/auth.spec.ts} +32 -56
  9. package/src/api/routes/{tests/configs.spec.js → global/tests/configs.spec.ts} +76 -47
  10. package/src/api/routes/{tests/email.spec.js → global/tests/email.spec.ts} +7 -15
  11. package/src/api/routes/{tests/realEmail.spec.js → global/tests/realEmail.spec.ts} +20 -21
  12. package/src/api/routes/{tests/self.spec.js → global/tests/self.spec.ts} +10 -18
  13. package/src/api/routes/global/tests/users.spec.ts +512 -0
  14. package/src/api/routes/index.js +2 -0
  15. package/src/api/routes/system/accounts.ts +19 -0
  16. package/src/api/routes/system/tests/accounts.spec.ts +57 -0
  17. package/src/{environment.js → environment.ts} +10 -9
  18. package/src/index.ts +6 -4
  19. package/src/sdk/accounts/accounts.ts +53 -0
  20. package/src/sdk/accounts/index.ts +1 -0
  21. package/src/sdk/index.ts +1 -0
  22. package/src/sdk/users/users.ts +271 -77
  23. package/src/tests/TestConfiguration.ts +273 -0
  24. package/src/tests/api/accounts.ts +28 -0
  25. package/src/tests/api/auth.ts +48 -0
  26. package/src/tests/api/configs.ts +40 -0
  27. package/src/tests/api/email.ts +24 -0
  28. package/src/tests/api/index.ts +25 -0
  29. package/src/tests/api/self.ts +21 -0
  30. package/src/tests/api/users.ts +111 -0
  31. package/src/tests/index.ts +14 -0
  32. package/src/tests/mocks/index.ts +7 -0
  33. package/src/tests/structures/accounts.ts +24 -0
  34. package/src/tests/structures/index.ts +20 -0
  35. package/src/tests/structures/users.ts +12 -4
  36. package/src/utilities/redis.js +1 -0
  37. package/scripts/load/users.js +0 -97
  38. package/src/api/routes/tests/users.spec.js +0 -390
  39. package/src/tests/TestConfiguration.js +0 -231
  40. package/src/tests/index.js +0 -12
  41. package/src/tests/mocks/index.js +0 -5
  42. package/src/tests/structures/index.js +0 -14
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "1.2.58",
4
+ "version": "1.3.1",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -35,10 +35,10 @@
35
35
  "author": "Budibase",
36
36
  "license": "GPL-3.0",
37
37
  "dependencies": {
38
- "@budibase/backend-core": "^1.2.58",
39
- "@budibase/pro": "1.2.57",
40
- "@budibase/string-templates": "^1.2.58",
41
- "@budibase/types": "^1.2.58",
38
+ "@budibase/backend-core": "^1.3.1",
39
+ "@budibase/pro": "1.2.59",
40
+ "@budibase/string-templates": "^1.3.1",
41
+ "@budibase/types": "^1.3.1",
42
42
  "@koa/router": "8.0.8",
43
43
  "@sentry/node": "6.17.7",
44
44
  "@techpass/passport-openidconnect": "0.3.2",
@@ -74,6 +74,7 @@
74
74
  "@types/koa-router": "7.4.4",
75
75
  "@types/koa__router": "8.0.11",
76
76
  "@types/node": "14.18.20",
77
+ "@types/uuid": "8.3.4",
77
78
  "@typescript-eslint/parser": "5.12.0",
78
79
  "copyfiles": "2.4.1",
79
80
  "eslint": "6.8.0",
@@ -102,5 +103,5 @@
102
103
  "./scripts/jestSetup.js"
103
104
  ]
104
105
  },
105
- "gitHead": "2922b47620173f41097d36b9dc5b1c0f8f7b063a"
106
+ "gitHead": "68d741c482a7a9c2ca2f4de6fedd809d06bd242b"
106
107
  }
@@ -14,3 +14,9 @@ const tk = require("timekeeper")
14
14
  tk.freeze(mocks.date.MOCK_DATE)
15
15
 
16
16
  global.console.log = jest.fn() // console.log are ignored in tests
17
+
18
+ if (!process.env.CI) {
19
+ // set a longer timeout in dev for debugging
20
+ // 100 seconds
21
+ jest.setTimeout(100000)
22
+ }
@@ -227,9 +227,22 @@ export const oidcPreAuth = async (ctx: any, next: any) => {
227
227
 
228
228
  setCookie(ctx, configId, Cookies.OIDC_CONFIG)
229
229
 
230
+ const db = getGlobalDB()
231
+ const config = await core.db.getScopedConfig(db, {
232
+ type: Configs.OIDC,
233
+ group: ctx.query.group,
234
+ })
235
+
236
+ const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
237
+
238
+ let authScopes =
239
+ chosenConfig.scopes?.length > 0
240
+ ? chosenConfig.scopes
241
+ : ["profile", "email", "offline_access"]
242
+
230
243
  return passport.authenticate(strategy, {
231
244
  // required 'openid' scope is added by oidc strategy factory
232
- scope: ["profile", "email", "offline_access"], //auth0 offline_access scope required for the refresh token behaviour.
245
+ scope: authScopes,
233
246
  })(ctx, next)
234
247
  }
235
248
 
@@ -1,9 +1,13 @@
1
- import { EmailTemplatePurpose } from "../../../constants"
2
1
  import { checkInviteCode } from "../../../utilities/redis"
3
- import { sendEmail } from "../../../utilities/email"
4
2
  import { users } from "../../../sdk"
5
3
  import env from "../../../environment"
6
- import { CloudAccount, User } from "@budibase/types"
4
+ import {
5
+ BulkDeleteUsersRequest,
6
+ CloudAccount,
7
+ InviteUserRequest,
8
+ InviteUsersRequest,
9
+ User,
10
+ } from "@budibase/types"
7
11
  import {
8
12
  accounts,
9
13
  cache,
@@ -46,8 +50,8 @@ export const bulkCreate = async (ctx: any) => {
46
50
  }
47
51
 
48
52
  try {
49
- let response = await users.bulkCreate(newUsersRequested, groups)
50
- await groupUtils.bulkSaveGroupUsers(groupsToSave, response)
53
+ const response = await users.bulkCreate(newUsersRequested, groups)
54
+ await groupUtils.bulkSaveGroupUsers(groupsToSave, response.successful)
51
55
 
52
56
  ctx.body = response
53
57
  } catch (err: any) {
@@ -138,17 +142,15 @@ export const destroy = async (ctx: any) => {
138
142
  }
139
143
 
140
144
  export const bulkDelete = async (ctx: any) => {
141
- const { userIds } = ctx.request.body
145
+ const { userIds } = ctx.request.body as BulkDeleteUsersRequest
142
146
  if (userIds?.indexOf(ctx.user._id) !== -1) {
143
147
  ctx.throw(400, "Unable to delete self.")
144
148
  }
145
149
 
146
150
  try {
147
- let usersResponse = await users.bulkDelete(userIds)
151
+ let response = await users.bulkDelete(userIds)
148
152
 
149
- ctx.body = {
150
- message: `${usersResponse.length} user(s) deleted`,
151
- }
153
+ ctx.body = response
152
154
  } catch (err) {
153
155
  ctx.throw(err)
154
156
  }
@@ -193,58 +195,27 @@ export const tenantUserLookup = async (ctx: any) => {
193
195
  }
194
196
 
195
197
  export const invite = async (ctx: any) => {
196
- let { email, userInfo } = ctx.request.body
197
- const existing = await usersCore.getGlobalUserByEmail(email)
198
- if (existing) {
199
- ctx.throw(400, "Email address already in use.")
200
- }
201
- if (!userInfo) {
202
- userInfo = {}
203
- }
204
- userInfo.tenantId = tenancy.getTenantId()
205
- const opts: any = {
206
- subject: "{{ company }} platform invitation",
207
- info: userInfo,
198
+ const request = ctx.request.body as InviteUserRequest
199
+ const response = await users.invite([request])
200
+
201
+ // explicitly throw for single user invite
202
+ if (response.unsuccessful.length) {
203
+ const reason = response.unsuccessful[0].reason
204
+ if (reason === "Unavailable") {
205
+ ctx.throw(400, reason)
206
+ } else {
207
+ ctx.throw(500, reason)
208
+ }
208
209
  }
209
- await sendEmail(email, EmailTemplatePurpose.INVITATION, opts)
210
+
210
211
  ctx.body = {
211
212
  message: "Invitation has been sent.",
212
213
  }
213
- await events.user.invited()
214
214
  }
215
215
 
216
216
  export const inviteMultiple = async (ctx: any) => {
217
- let users = ctx.request.body
218
- let existing = false
219
- let existingEmail
220
- for (let user of users) {
221
- if (await usersCore.getGlobalUserByEmail(user.email)) {
222
- existing = true
223
- existingEmail = user.email
224
- break
225
- }
226
- }
227
-
228
- if (existing) {
229
- ctx.throw(400, `${existingEmail} already exists`)
230
- }
231
-
232
- for (let i = 0; i < users.length; i++) {
233
- let userInfo = users[i].userInfo
234
- if (!userInfo) {
235
- userInfo = {}
236
- }
237
- userInfo.tenantId = tenancy.getTenantId()
238
- const opts: any = {
239
- subject: "{{ company }} platform invitation",
240
- info: userInfo,
241
- }
242
- await sendEmail(users[i].email, EmailTemplatePurpose.INVITATION, opts)
243
- }
244
-
245
- ctx.body = {
246
- message: "Invitations have been sent.",
247
- }
217
+ const request = ctx.request.body as InviteUsersRequest
218
+ ctx.body = await users.invite(request)
248
219
  }
249
220
 
250
221
  export const inviteAccept = async (ctx: any) => {
@@ -0,0 +1,21 @@
1
+ import { Account, AccountMetadata } from "@budibase/types"
2
+ import { accounts } from "../../../sdk"
3
+
4
+ export const save = async (ctx: any) => {
5
+ const account = ctx.request.body as Account
6
+ let metadata: AccountMetadata = {
7
+ _id: accounts.formatAccountMetadataId(account.accountId),
8
+ email: account.email,
9
+ }
10
+
11
+ metadata = await accounts.saveMetadata(metadata)
12
+
13
+ ctx.body = metadata
14
+ ctx.status = 200
15
+ }
16
+
17
+ export const destroy = async (ctx: any) => {
18
+ const accountId = accounts.formatAccountMetadataId(ctx.params.accountId)
19
+ await accounts.destroyMetadata(accountId)
20
+ ctx.status = 204
21
+ }
@@ -1,15 +1,10 @@
1
- const Router = require("@koa/router")
1
+ import Router from "@koa/router"
2
2
  const compress = require("koa-compress")
3
3
  const zlib = require("zlib")
4
- const { routes } = require("./routes")
5
- const {
6
- buildAuthMiddleware,
7
- auditLog,
8
- buildTenancyMiddleware,
9
- buildCsrfMiddleware,
10
- } = require("@budibase/backend-core/auth")
11
- const { middleware: pro } = require("@budibase/pro")
12
- const { errors } = require("@budibase/backend-core")
4
+ import { routes } from "./routes"
5
+ import { middleware as pro } from "@budibase/pro"
6
+ import { errors, auth, middleware } from "@budibase/backend-core"
7
+ import { APIError } from "@budibase/types"
13
8
 
14
9
  const PUBLIC_ENDPOINTS = [
15
10
  // old deprecated endpoints kept for backwards compat
@@ -97,9 +92,9 @@ router
97
92
  })
98
93
  )
99
94
  .use("/health", ctx => (ctx.status = 200))
100
- .use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
101
- .use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
102
- .use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
95
+ .use(auth.buildAuthMiddleware(PUBLIC_ENDPOINTS))
96
+ .use(auth.buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
97
+ .use(auth.buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
103
98
  .use(pro.licensing())
104
99
  // for now no public access is allowed to worker (bar health check)
105
100
  .use((ctx, next) => {
@@ -114,21 +109,22 @@ router
114
109
  }
115
110
  return next()
116
111
  })
117
- .use(auditLog)
112
+ .use(middleware.auditLog)
118
113
 
119
114
  // error handling middleware - TODO: This could be moved to backend-core
120
115
  router.use(async (ctx, next) => {
121
116
  try {
122
117
  await next()
123
- } catch (err) {
118
+ } catch (err: any) {
124
119
  ctx.log.error(err)
125
120
  ctx.status = err.status || err.statusCode || 500
126
121
  const error = errors.getPublicError(err)
127
- ctx.body = {
122
+ const body: APIError = {
128
123
  message: err.message,
129
124
  status: ctx.status,
130
125
  error,
131
126
  }
127
+ ctx.body = body
132
128
  }
133
129
  })
134
130
 
@@ -53,6 +53,7 @@ function oidcValidation() {
53
53
  name: Joi.string().allow("", null),
54
54
  uuid: Joi.string().required(),
55
55
  activated: Joi.boolean().required(),
56
+ scopes: Joi.array().optional()
56
57
  })
57
58
  ).required(true)
58
59
  }).unknown(true)
@@ -1,11 +1,11 @@
1
1
  jest.mock("nodemailer")
2
- const { config, request, mocks, structures } = require("../../../tests")
2
+ import { TestConfiguration, mocks, API } from "../../../../tests"
3
3
  const sendMailMock = mocks.email.mock()
4
- const { events } = require("@budibase/backend-core")
5
-
6
- const TENANT_ID = structures.TENANT_ID
4
+ import { events } from "@budibase/backend-core"
7
5
 
8
6
  describe("/api/global/auth", () => {
7
+ const config = new TestConfiguration()
8
+ const api = new API(config)
9
9
 
10
10
  beforeAll(async () => {
11
11
  await config.beforeAll()
@@ -19,56 +19,32 @@ describe("/api/global/auth", () => {
19
19
  jest.clearAllMocks()
20
20
  })
21
21
 
22
- const requestPasswordReset = async () => {
23
- await config.saveSmtpConfig()
24
- await config.saveSettingsConfig()
25
- await config.createUser()
26
- const res = await request
27
- .post(`/api/global/auth/${TENANT_ID}/reset`)
28
- .send({
29
- email: "test@test.com",
30
- })
31
- .expect("Content-Type", /json/)
32
- .expect(200)
33
- const emailCall = sendMailMock.mock.calls[0][0]
34
- const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`)
35
- const code = parts[1].split("\"")[0].split("&")[0]
36
- return { code, res }
37
- }
38
-
39
22
  it("should logout", async () => {
40
- await request
41
- .post("/api/global/auth/logout")
42
- .set(config.defaultHeaders())
43
- .expect(200)
23
+ await api.auth.logout()
44
24
  expect(events.auth.logout).toBeCalledTimes(1)
45
25
  })
46
26
 
47
27
  it("should be able to generate password reset email", async () => {
48
- const { res, code } = await requestPasswordReset()
28
+ const { res, code } = await api.auth.requestPasswordReset(sendMailMock)
49
29
  const user = await config.getUser("test@test.com")
50
30
 
51
- expect(res.body).toEqual({ message: "Please check your email for a reset link." })
31
+ expect(res.body).toEqual({
32
+ message: "Please check your email for a reset link.",
33
+ })
52
34
  expect(sendMailMock).toHaveBeenCalled()
53
-
35
+
54
36
  expect(code).toBeDefined()
55
37
  expect(events.user.passwordResetRequested).toBeCalledTimes(1)
56
38
  expect(events.user.passwordResetRequested).toBeCalledWith(user)
57
39
  })
58
40
 
59
41
  it("should allow resetting user password with code", async () => {
60
- const { code } = await requestPasswordReset()
42
+ const { code } = await api.auth.requestPasswordReset(sendMailMock)
61
43
  const user = await config.getUser("test@test.com")
62
- delete user.password
44
+ delete user.password
45
+
46
+ const res = await api.auth.updatePassword(code)
63
47
 
64
- const res = await request
65
- .post(`/api/global/auth/${TENANT_ID}/reset/update`)
66
- .send({
67
- password: "newpassword",
68
- resetCode: code,
69
- })
70
- .expect("Content-Type", /json/)
71
- .expect(200)
72
48
  expect(res.body).toEqual({ message: "password reset successfully." })
73
49
  expect(events.user.passwordReset).toBeCalledTimes(1)
74
50
  expect(events.user.passwordReset).toBeCalledWith(user)
@@ -79,15 +55,15 @@ describe("/api/global/auth", () => {
79
55
 
80
56
  const passportSpy = jest.spyOn(auth.passport, "authenticate")
81
57
  let oidcConf
82
- let chosenConfig
83
- let configId
58
+ let chosenConfig: any
59
+ let configId: string
84
60
 
85
61
  // mock the oidc strategy implementation and return value
86
62
  let strategyFactory = jest.fn()
87
63
  let mockStrategyReturn = jest.fn()
88
64
  let mockStrategyConfig = jest.fn()
89
65
  auth.oidc.fetchStrategyConfig = mockStrategyConfig
90
-
66
+
91
67
  strategyFactory.mockReturnValue(mockStrategyReturn)
92
68
  auth.oidc.strategyFactory = strategyFactory
93
69
 
@@ -99,34 +75,34 @@ describe("/api/global/auth", () => {
99
75
  })
100
76
 
101
77
  afterEach(() => {
102
- expect(strategyFactory).toBeCalledWith(
103
- chosenConfig,
104
- expect.any(Function)
105
- )
78
+ expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function))
106
79
  })
107
80
 
108
81
  describe("oidc configs", () => {
109
82
  it("should load strategy and delegate to passport", async () => {
110
- await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`)
83
+ await api.configs.getOIDCConfig(configId)
111
84
 
112
85
  expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
113
- scope: ["profile", "email", "offline_access"]
86
+ scope: ["profile", "email", "offline_access"],
114
87
  })
115
- expect(passportSpy.mock.calls.length).toBe(1);
88
+ expect(passportSpy.mock.calls.length).toBe(1)
116
89
  })
117
90
  })
118
91
 
119
92
  describe("oidc callback", () => {
120
93
  it("should load strategy and delegate to passport", async () => {
121
- await request.get(`/api/global/auth/${TENANT_ID}/oidc/callback`)
122
- .set(config.getOIDConfigCookie(configId))
123
-
124
- expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
125
- successRedirect: "/", failureRedirect: "/error"
126
- }, expect.anything())
127
- expect(passportSpy.mock.calls.length).toBe(1);
94
+ await api.configs.OIDCCallback(configId)
95
+
96
+ expect(passportSpy).toBeCalledWith(
97
+ mockStrategyReturn,
98
+ {
99
+ successRedirect: "/",
100
+ failureRedirect: "/error",
101
+ },
102
+ expect.anything()
103
+ )
104
+ expect(passportSpy.mock.calls.length).toBe(1)
128
105
  })
129
106
  })
130
-
131
107
  })
132
108
  })