@budibase/worker 2.3.17-alpha.4 → 2.3.17-alpha.5

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 (43) hide show
  1. package/package.json +8 -7
  2. package/scripts/dev/manage.js +1 -0
  3. package/src/api/controllers/global/auth.ts +77 -70
  4. package/src/api/controllers/global/self.ts +28 -44
  5. package/src/api/controllers/global/users.ts +65 -25
  6. package/src/api/controllers/system/accounts.ts +7 -5
  7. package/src/api/controllers/system/tenants.ts +4 -8
  8. package/src/api/index.ts +4 -20
  9. package/src/api/routes/global/auth.ts +10 -7
  10. package/src/api/routes/global/tests/auth.spec.ts +234 -33
  11. package/src/api/routes/global/tests/configs.spec.ts +93 -113
  12. package/src/api/routes/global/tests/roles.spec.ts +3 -2
  13. package/src/api/routes/global/tests/self.spec.ts +3 -4
  14. package/src/api/routes/global/tests/users.spec.ts +44 -46
  15. package/src/api/routes/system/tenants.ts +1 -1
  16. package/src/api/routes/system/tests/accounts.spec.ts +4 -4
  17. package/src/api/routes/system/tests/migrations.spec.ts +2 -2
  18. package/src/api/routes/system/tests/restore.spec.ts +2 -2
  19. package/src/api/routes/system/tests/status.spec.ts +5 -5
  20. package/src/environment.ts +15 -1
  21. package/src/index.ts +6 -0
  22. package/src/middleware/tests/tenancy.spec.ts +4 -4
  23. package/src/migrations/functions/globalInfoSyncUsers.ts +4 -3
  24. package/src/sdk/accounts/index.ts +2 -1
  25. package/src/sdk/accounts/{accounts.ts → metadata.ts} +0 -1
  26. package/src/sdk/auth/auth.ts +86 -0
  27. package/src/sdk/auth/index.ts +1 -0
  28. package/src/sdk/tenants/index.ts +1 -0
  29. package/src/sdk/tenants/tenants.ts +76 -0
  30. package/src/sdk/users/events.ts +4 -0
  31. package/src/sdk/users/index.ts +1 -0
  32. package/src/sdk/users/tests/users.spec.ts +52 -0
  33. package/src/sdk/users/users.ts +53 -46
  34. package/src/tests/TestConfiguration.ts +38 -89
  35. package/src/tests/api/auth.ts +42 -18
  36. package/src/tests/api/base.ts +2 -1
  37. package/src/tests/api/restore.ts +1 -0
  38. package/src/tests/jestEnv.ts +2 -1
  39. package/src/tests/jestSetup.ts +2 -5
  40. package/src/tests/logging.ts +34 -0
  41. package/src/tests/structures/index.ts +0 -2
  42. package/src/utilities/email.ts +3 -3
  43. package/src/tests/structures/users.ts +0 -37
@@ -1,21 +1,23 @@
1
1
  import { Account, AccountMetadata } from "@budibase/types"
2
- import sdk from "../../../sdk"
2
+ import * as accounts from "../../../sdk/accounts"
3
3
 
4
4
  export const save = async (ctx: any) => {
5
5
  const account = ctx.request.body as Account
6
6
  let metadata: AccountMetadata = {
7
- _id: sdk.accounts.formatAccountMetadataId(account.accountId),
7
+ _id: accounts.metadata.formatAccountMetadataId(account.accountId),
8
8
  email: account.email,
9
9
  }
10
10
 
11
- metadata = await sdk.accounts.saveMetadata(metadata)
11
+ metadata = await accounts.metadata.saveMetadata(metadata)
12
12
 
13
13
  ctx.body = metadata
14
14
  ctx.status = 200
15
15
  }
16
16
 
17
17
  export const destroy = async (ctx: any) => {
18
- const accountId = sdk.accounts.formatAccountMetadataId(ctx.params.accountId)
19
- await sdk.accounts.destroyMetadata(accountId)
18
+ const accountId = accounts.metadata.formatAccountMetadataId(
19
+ ctx.params.accountId
20
+ )
21
+ await accounts.metadata.destroyMetadata(accountId)
20
22
  ctx.status = 204
21
23
  }
@@ -1,8 +1,7 @@
1
- import { BBContext } from "@budibase/types"
2
- import { deprovisioning } from "@budibase/backend-core"
3
- import { quotas } from "@budibase/pro"
1
+ import { UserCtx } from "@budibase/types"
2
+ import * as tenantSdk from "../../../sdk/tenants"
4
3
 
5
- const _delete = async (ctx: BBContext) => {
4
+ export async function destroy(ctx: UserCtx) {
6
5
  const user = ctx.user!
7
6
  const tenantId = ctx.params.tenantId
8
7
 
@@ -11,13 +10,10 @@ const _delete = async (ctx: BBContext) => {
11
10
  }
12
11
 
13
12
  try {
14
- await quotas.bustCache()
15
- await deprovisioning.deleteTenant(tenantId)
13
+ await tenantSdk.deleteTenant(tenantId)
16
14
  ctx.status = 204
17
15
  } catch (err) {
18
16
  ctx.log.error(err)
19
17
  throw err
20
18
  }
21
19
  }
22
-
23
- export { _delete as delete }
package/src/api/index.ts CHANGED
@@ -3,8 +3,7 @@ const compress = require("koa-compress")
3
3
  const zlib = require("zlib")
4
4
  import { routes } from "./routes"
5
5
  import { middleware as pro } from "@budibase/pro"
6
- import { errors, auth, middleware } from "@budibase/backend-core"
7
- import { APIError } from "@budibase/types"
6
+ import { auth, middleware } from "@budibase/backend-core"
8
7
 
9
8
  const PUBLIC_ENDPOINTS = [
10
9
  // deprecated single tenant sso callback
@@ -109,7 +108,9 @@ const NO_TENANCY_ENDPOINTS = [
109
108
  const NO_CSRF_ENDPOINTS = [...PUBLIC_ENDPOINTS]
110
109
 
111
110
  const router: Router = new Router()
111
+
112
112
  router
113
+ .use(middleware.errorHandling)
113
114
  .use(
114
115
  compress({
115
116
  threshold: 2048,
@@ -136,29 +137,12 @@ router
136
137
  (!ctx.isAuthenticated || (ctx.user && !ctx.user.budibaseAccess)) &&
137
138
  !ctx.internal
138
139
  ) {
139
- ctx.throw(403, "Unauthorized - no public worker access")
140
+ ctx.throw(403, "Unauthorized")
140
141
  }
141
142
  return next()
142
143
  })
143
144
  .use(middleware.auditLog)
144
145
 
145
- // error handling middleware - TODO: This could be moved to backend-core
146
- router.use(async (ctx, next) => {
147
- try {
148
- await next()
149
- } catch (err: any) {
150
- ctx.log.error(err)
151
- ctx.status = err.status || err.statusCode || 500
152
- const error = errors.getPublicError(err)
153
- const body: APIError = {
154
- message: err.message,
155
- status: ctx.status,
156
- error,
157
- }
158
- ctx.body = body
159
- }
160
- })
161
-
162
146
  router.get("/health", ctx => (ctx.status = 200))
163
147
 
164
148
  // authenticated routes
@@ -33,7 +33,7 @@ router
33
33
  .post(
34
34
  "/api/global/auth/:tenantId/login",
35
35
  buildAuthValidation(),
36
- authController.authenticate
36
+ authController.login
37
37
  )
38
38
  .post("/api/global/auth/logout", authController.logout)
39
39
  .post(
@@ -68,21 +68,24 @@ router
68
68
 
69
69
  // GOOGLE - MULTI TENANT
70
70
  .get("/api/global/auth/:tenantId/google", authController.googlePreAuth)
71
- .get("/api/global/auth/:tenantId/google/callback", authController.googleAuth)
71
+ .get(
72
+ "/api/global/auth/:tenantId/google/callback",
73
+ authController.googleCallback
74
+ )
72
75
 
73
76
  // GOOGLE - SINGLE TENANT - DEPRECATED
74
- .get("/api/global/auth/google/callback", authController.googleAuth)
75
- .get("/api/admin/auth/google/callback", authController.googleAuth)
77
+ .get("/api/global/auth/google/callback", authController.googleCallback)
78
+ .get("/api/admin/auth/google/callback", authController.googleCallback)
76
79
 
77
80
  // OIDC - MULTI TENANT
78
81
  .get(
79
82
  "/api/global/auth/:tenantId/oidc/configs/:configId",
80
83
  authController.oidcPreAuth
81
84
  )
82
- .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth)
85
+ .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcCallback)
83
86
 
84
87
  // OIDC - SINGLE TENANT - DEPRECATED
85
- .get("/api/global/auth/oidc/callback", authController.oidcAuth)
86
- .get("/api/admin/auth/oidc/callback", authController.oidcAuth)
88
+ .get("/api/global/auth/oidc/callback", authController.oidcCallback)
89
+ .get("/api/admin/auth/oidc/callback", authController.oidcCallback)
87
90
 
88
91
  export default router
@@ -1,13 +1,27 @@
1
+ import { CloudAccount, SSOUser, User } from "@budibase/types"
2
+
1
3
  jest.mock("nodemailer")
2
- import { TestConfiguration, mocks } from "../../../../tests"
4
+ import {
5
+ TestConfiguration,
6
+ mocks,
7
+ structures,
8
+ generator,
9
+ } from "../../../../tests"
3
10
  const sendMailMock = mocks.email.mock()
4
- import { events, tenancy } from "@budibase/backend-core"
5
- import { structures } from "@budibase/backend-core/tests"
11
+ import { events, constants } from "@budibase/backend-core"
12
+ import { Response } from "superagent"
13
+
14
+ import * as userSdk from "../../../../sdk/users"
15
+
16
+ function getAuthCookie(response: Response) {
17
+ return response.headers["set-cookie"]
18
+ .find((s: string) => s.startsWith(`${constants.Cookie.Auth}=`))
19
+ .split("=")[1]
20
+ .split(";")[0]
21
+ }
6
22
 
7
- const expectSetAuthCookie = (res: any) => {
8
- expect(
9
- res.get("Set-Cookie").find((c: string) => c.startsWith("budibase:auth"))
10
- ).toBeDefined()
23
+ const expectSetAuthCookie = (response: Response) => {
24
+ expect(getAuthCookie(response).length > 1).toBe(true)
11
25
  }
12
26
 
13
27
  describe("/api/global/auth", () => {
@@ -25,60 +39,247 @@ describe("/api/global/auth", () => {
25
39
  jest.clearAllMocks()
26
40
  })
27
41
 
42
+ async function createSSOUser() {
43
+ return config.doInTenant(async () => {
44
+ return userSdk.save(structures.users.ssoUser(), {
45
+ requirePassword: false,
46
+ })
47
+ })
48
+ }
49
+
28
50
  describe("password", () => {
29
51
  describe("POST /api/global/auth/:tenantId/login", () => {
30
- it("should login", () => {})
52
+ it("logs in with correct credentials", async () => {
53
+ const tenantId = config.tenantId!
54
+ const email = config.user?.email!
55
+ const password = config.userPassword
56
+
57
+ const response = await config.api.auth.login(tenantId, email, password)
58
+
59
+ expectSetAuthCookie(response)
60
+ expect(events.auth.login).toBeCalledTimes(1)
61
+ })
62
+
63
+ it("should return 403 with incorrect credentials", async () => {
64
+ const tenantId = config.tenantId!
65
+ const email = config.user?.email!
66
+ const password = "incorrect"
67
+
68
+ const response = await config.api.auth.login(
69
+ tenantId,
70
+ email,
71
+ password,
72
+ { status: 403 }
73
+ )
74
+ expect(response.body).toEqual({
75
+ message: "Invalid credentials",
76
+ status: 403,
77
+ })
78
+ })
79
+
80
+ it("should return 403 when user doesn't exist", async () => {
81
+ const tenantId = config.tenantId!
82
+ const email = "invaliduser@test.com"
83
+ const password = "password"
84
+
85
+ const response = await config.api.auth.login(
86
+ tenantId,
87
+ email,
88
+ password,
89
+ { status: 403 }
90
+ )
91
+ expect(response.body).toEqual({
92
+ message: "Invalid credentials",
93
+ status: 403,
94
+ })
95
+ })
96
+
97
+ describe("sso user", () => {
98
+ let user: User
99
+
100
+ async function testSSOUser() {
101
+ const tenantId = user.tenantId!
102
+ const email = user.email
103
+ const password = "test"
104
+
105
+ const response = await config.api.auth.login(
106
+ tenantId,
107
+ email,
108
+ password,
109
+ { status: 400 }
110
+ )
111
+
112
+ expect(response.body).toEqual({
113
+ message: "SSO user cannot login using password",
114
+ status: 400,
115
+ })
116
+ }
117
+
118
+ describe("budibase sso user", () => {
119
+ it("should prevent user from logging in", async () => {
120
+ user = await createSSOUser()
121
+ await testSSOUser()
122
+ })
123
+ })
124
+
125
+ describe("root account sso user", () => {
126
+ it("should prevent user from logging in", async () => {
127
+ user = await config.createUser()
128
+ const account = structures.accounts.ssoAccount() as CloudAccount
129
+ mocks.accounts.getAccount.mockReturnValueOnce(
130
+ Promise.resolve(account)
131
+ )
132
+
133
+ await testSSOUser()
134
+ })
135
+ })
136
+ })
31
137
  })
32
138
 
33
139
  describe("POST /api/global/auth/logout", () => {
34
140
  it("should logout", async () => {
35
- await config.api.auth.logout()
141
+ const response = await config.api.auth.logout()
36
142
  expect(events.auth.logout).toBeCalledTimes(1)
37
143
 
38
- // TODO: Verify sessions deleted
144
+ const authCookie = getAuthCookie(response)
145
+ expect(authCookie).toBe("")
39
146
  })
40
147
  })
41
148
 
42
149
  describe("POST /api/global/auth/:tenantId/reset", () => {
43
150
  it("should generate password reset email", async () => {
44
- await tenancy.doInTenant(config.tenant1User!.tenantId, async () => {
45
- const userEmail = structures.email()
46
- const { res, code } = await config.api.auth.requestPasswordReset(
151
+ const user = await config.createUser()
152
+
153
+ const { res, code } = await config.api.auth.requestPasswordReset(
154
+ sendMailMock,
155
+ user.email
156
+ )
157
+
158
+ expect(res.body).toEqual({
159
+ message: "Please check your email for a reset link.",
160
+ })
161
+ expect(sendMailMock).toHaveBeenCalled()
162
+ expect(code).toBeDefined()
163
+ expect(events.user.passwordResetRequested).toBeCalledTimes(1)
164
+ expect(events.user.passwordResetRequested).toBeCalledWith(user)
165
+ })
166
+
167
+ describe("sso user", () => {
168
+ let user: User
169
+
170
+ async function testSSOUser() {
171
+ const { res } = await config.api.auth.requestPasswordReset(
47
172
  sendMailMock,
48
- userEmail
173
+ user.email,
174
+ { status: 400 }
49
175
  )
50
- const user = await config.getUser(userEmail)
51
176
 
52
177
  expect(res.body).toEqual({
53
- message: "Please check your email for a reset link.",
178
+ message: "SSO user cannot reset password",
179
+ status: 400,
180
+ error: {
181
+ code: "http",
182
+ type: "generic",
183
+ },
54
184
  })
55
- expect(sendMailMock).toHaveBeenCalled()
185
+ expect(sendMailMock).not.toHaveBeenCalled()
186
+ }
56
187
 
57
- expect(code).toBeDefined()
58
- expect(events.user.passwordResetRequested).toBeCalledTimes(1)
59
- expect(events.user.passwordResetRequested).toBeCalledWith(user)
188
+ describe("budibase sso user", () => {
189
+ it("should prevent user from generating password reset email", async () => {
190
+ user = await createSSOUser()
191
+ await testSSOUser()
192
+ })
193
+ })
194
+
195
+ describe("root account sso user", () => {
196
+ it("should prevent user from generating password reset email", async () => {
197
+ user = await config.createUser(structures.users.user())
198
+ const account = structures.accounts.ssoAccount() as CloudAccount
199
+ mocks.accounts.getAccount.mockReturnValueOnce(
200
+ Promise.resolve(account)
201
+ )
202
+
203
+ await testSSOUser()
204
+ })
60
205
  })
61
206
  })
62
207
  })
63
208
 
64
209
  describe("POST /api/global/auth/:tenantId/reset/update", () => {
65
210
  it("should reset password", async () => {
66
- await tenancy.doInTenant(config.tenant1User!.tenantId, async () => {
67
- const userEmail = structures.email()
68
- const { code } = await config.api.auth.requestPasswordReset(
69
- sendMailMock,
70
- userEmail
211
+ let user = await config.createUser()
212
+ const { code } = await config.api.auth.requestPasswordReset(
213
+ sendMailMock,
214
+ user.email
215
+ )
216
+ delete user.password
217
+
218
+ const newPassword = "newpassword"
219
+ const res = await config.api.auth.updatePassword(code!, newPassword)
220
+
221
+ user = await config.getUser(user.email)
222
+ delete user.password
223
+
224
+ expect(res.body).toEqual({ message: "password reset successfully." })
225
+ expect(events.user.passwordReset).toBeCalledTimes(1)
226
+ expect(events.user.passwordReset).toBeCalledWith(user)
227
+
228
+ // login using new password
229
+ await config.api.auth.login(user.tenantId, user.email, newPassword)
230
+ })
231
+
232
+ describe("sso user", () => {
233
+ let user: User | SSOUser
234
+
235
+ async function testSSOUser(code: string) {
236
+ const res = await config.api.auth.updatePassword(
237
+ code!,
238
+ generator.string(),
239
+ { status: 400 }
71
240
  )
72
- const user = await config.getUser(userEmail)
73
- delete user.password
74
241
 
75
- const res = await config.api.auth.updatePassword(code)
242
+ expect(res.body).toEqual({
243
+ message: "Cannot reset password.",
244
+ status: 400,
245
+ })
246
+ }
247
+
248
+ describe("budibase sso user", () => {
249
+ it("should prevent user from generating password reset email", async () => {
250
+ user = await config.createUser()
251
+ const { code } = await config.api.auth.requestPasswordReset(
252
+ sendMailMock,
253
+ user.email
254
+ )
255
+
256
+ // convert to sso now that password reset has been requested
257
+ const ssoUser = user as SSOUser
258
+ ssoUser.providerType = structures.sso.providerType()
259
+ delete ssoUser.password
260
+ await config.doInTenant(() => userSdk.save(ssoUser))
261
+
262
+ await testSSOUser(code!)
263
+ })
264
+ })
76
265
 
77
- expect(res.body).toEqual({ message: "password reset successfully." })
78
- expect(events.user.passwordReset).toBeCalledTimes(1)
79
- expect(events.user.passwordReset).toBeCalledWith(user)
266
+ describe("root account sso user", () => {
267
+ it("should prevent user from generating password reset email", async () => {
268
+ user = await config.createUser()
269
+ const { code } = await config.api.auth.requestPasswordReset(
270
+ sendMailMock,
271
+ user.email
272
+ )
273
+
274
+ // convert to account owner now that password has been requested
275
+ const account = structures.accounts.ssoAccount() as CloudAccount
276
+ mocks.accounts.getAccount.mockReturnValueOnce(
277
+ Promise.resolve(account)
278
+ )
279
+
280
+ await testSSOUser(code!)
281
+ })
80
282
  })
81
- // TODO: Login using new password
82
283
  })
83
284
  })
84
285
  })
@@ -153,7 +354,7 @@ describe("/api/global/auth", () => {
153
354
  const location: string = res.get("location")
154
355
  expect(
155
356
  location.startsWith(
156
- "http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2Fdefault%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access"
357
+ `http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2F${config.tenantId}%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access`
157
358
  )
158
359
  ).toBe(true)
159
360
  })