@budibase/worker 2.8.29-alpha.0 → 2.8.29-alpha.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,602 +1,19 @@
1
- import env from "../../environment"
2
- import * as eventHelpers from "./events"
3
- import {
4
- accounts,
5
- cache,
6
- constants,
7
- db as dbUtils,
8
- events,
9
- HTTPError,
10
- sessions,
11
- tenancy,
12
- platform,
13
- users as usersCore,
14
- utils,
15
- ViewName,
16
- env as coreEnv,
17
- context,
18
- EmailUnavailableError,
19
- } from "@budibase/backend-core"
20
- import {
21
- AccountMetadata,
22
- AllDocsResponse,
23
- CloudAccount,
24
- InviteUsersRequest,
25
- InviteUsersResponse,
26
- isSSOAccount,
27
- isSSOUser,
28
- PlatformUser,
29
- PlatformUserByEmail,
30
- RowResponse,
31
- User,
32
- SaveUserOpts,
33
- BulkUserCreated,
34
- BulkUserDeleted,
35
- Account,
36
- } from "@budibase/types"
1
+ import { events, tenancy, users as usersCore } from "@budibase/backend-core"
2
+ import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types"
37
3
  import { sendEmail } from "../../utilities/email"
38
4
  import { EmailTemplatePurpose } from "../../constants"
39
- import * as pro from "@budibase/pro"
40
- import * as accountSdk from "../accounts"
41
5
 
42
- export const allUsers = async () => {
43
- const db = tenancy.getGlobalDB()
44
- const response = await db.allDocs(
45
- dbUtils.getGlobalUserParams(null, {
46
- include_docs: true,
47
- })
48
- )
49
- return response.rows.map((row: any) => row.doc)
50
- }
51
-
52
- export const countUsersByApp = async (appId: string) => {
53
- let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
54
- return {
55
- userCount: response.length,
56
- }
57
- }
58
-
59
- export const getUsersByAppAccess = async (appId?: string) => {
60
- const opts: any = {
61
- include_docs: true,
62
- limit: 50,
63
- }
64
- let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
65
- appId,
66
- opts
67
- )
68
- return response
69
- }
70
-
71
- export async function getUserByEmail(email: string) {
72
- return usersCore.getGlobalUserByEmail(email)
73
- }
74
-
75
- /**
76
- * Gets a user by ID from the global database, based on the current tenancy.
77
- */
78
- export const getUser = async (userId: string) => {
79
- const user = await usersCore.getById(userId)
80
- if (user) {
81
- delete user.password
82
- }
83
- return user
84
- }
85
-
86
- const buildUser = async (
87
- user: User,
88
- opts: SaveUserOpts = {
89
- hashPassword: true,
90
- requirePassword: true,
91
- },
92
- tenantId: string,
93
- dbUser?: any,
94
- account?: Account
95
- ): Promise<User> => {
96
- let { password, _id } = user
97
-
98
- // don't require a password if the db user doesn't already have one
99
- if (dbUser && !dbUser.password) {
100
- opts.requirePassword = false
101
- }
102
-
103
- let hashedPassword
104
- if (password) {
105
- if (await isPreventPasswordActions(user, account)) {
106
- throw new HTTPError("Password change is disabled for this user", 400)
107
- }
108
- hashedPassword = opts.hashPassword ? await utils.hash(password) : password
109
- } else if (dbUser) {
110
- hashedPassword = dbUser.password
111
- }
112
-
113
- // passwords are never required if sso is enforced
114
- const requirePasswords =
115
- opts.requirePassword && !(await pro.features.isSSOEnforced())
116
- if (!hashedPassword && requirePasswords) {
117
- throw "Password must be specified."
118
- }
119
-
120
- _id = _id || dbUtils.generateGlobalUserID()
121
-
122
- const fullUser = {
123
- createdAt: Date.now(),
124
- ...dbUser,
125
- ...user,
126
- _id,
127
- password: hashedPassword,
128
- tenantId,
129
- }
130
- // make sure the roles object is always present
131
- if (!fullUser.roles) {
132
- fullUser.roles = {}
133
- }
134
- // add the active status to a user if its not provided
135
- if (fullUser.status == null) {
136
- fullUser.status = constants.UserStatus.ACTIVE
137
- }
138
-
139
- return fullUser
140
- }
141
-
142
- // lookup, could be email or userId, either will return a doc
143
- export const getPlatformUser = async (
144
- identifier: string
145
- ): Promise<PlatformUser | null> => {
146
- // use the view here and allow to find anyone regardless of casing
147
- // Use lowercase to ensure email login is case insensitive
148
- const response = dbUtils.queryPlatformView(
149
- ViewName.PLATFORM_USERS_LOWERCASE,
150
- {
151
- keys: [identifier.toLowerCase()],
152
- include_docs: true,
153
- }
154
- ) as Promise<PlatformUser>
155
- return response
156
- }
157
-
158
- const validateUniqueUser = async (email: string, tenantId: string) => {
159
- // check budibase users in other tenants
160
- if (env.MULTI_TENANCY) {
161
- const tenantUser = await getPlatformUser(email)
162
- if (tenantUser != null && tenantUser.tenantId !== tenantId) {
163
- throw new EmailUnavailableError(email)
164
- }
165
- }
166
-
167
- // check root account users in account portal
168
- if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
169
- const account = await accounts.getAccount(email)
170
- if (account && account.verified && account.tenantId !== tenantId) {
171
- throw new EmailUnavailableError(email)
172
- }
173
- }
174
- }
175
-
176
- export async function isPreventPasswordActions(user: User, account?: Account) {
177
- // when in maintenance mode we allow sso users with the admin role
178
- // to perform any password action - this prevents lockout
179
- if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
180
- return false
181
- }
182
-
183
- // SSO is enforced for all users
184
- if (await pro.features.isSSOEnforced()) {
185
- return true
186
- }
187
-
188
- // Check local sso
189
- if (isSSOUser(user)) {
190
- return true
191
- }
192
-
193
- // Check account sso
194
- if (!account) {
195
- account = await accountSdk.api.getAccountByTenantId(tenancy.getTenantId())
196
- }
197
- return !!(account && account.email === user.email && isSSOAccount(account))
198
- }
199
-
200
- // TODO: The single save should re-use the bulk insert with a single
201
- // user so that we don't need to duplicate logic
202
- export const save = async (
203
- user: User,
204
- opts: SaveUserOpts = {}
205
- ): Promise<User> => {
206
- // default booleans to true
207
- if (opts.hashPassword == null) {
208
- opts.hashPassword = true
209
- }
210
- if (opts.requirePassword == null) {
211
- opts.requirePassword = true
212
- }
213
- const tenantId = tenancy.getTenantId()
214
- const db = tenancy.getGlobalDB()
215
-
216
- let { email, _id, userGroups = [], roles } = user
217
-
218
- if (!email && !_id) {
219
- throw new Error("_id or email is required")
220
- }
221
-
222
- let dbUser: User | undefined
223
- if (_id) {
224
- // try to get existing user from db
225
- try {
226
- dbUser = (await db.get(_id)) as User
227
- if (email && dbUser.email !== email) {
228
- throw "Email address cannot be changed"
229
- }
230
- email = dbUser.email
231
- } catch (e: any) {
232
- if (e.status === 404) {
233
- // do nothing, save this new user with the id specified - required for SSO auth
234
- } else {
235
- throw e
236
- }
237
- }
238
- }
239
-
240
- if (!dbUser && email) {
241
- // no id was specified - load from email instead
242
- dbUser = await usersCore.getGlobalUserByEmail(email)
243
- if (dbUser && dbUser._id !== _id) {
244
- throw new EmailUnavailableError(email)
245
- }
246
- }
247
-
248
- const change = dbUser ? 0 : 1 // no change if there is existing user
249
- return pro.quotas.addUsers(change, async () => {
250
- await validateUniqueUser(email, tenantId)
251
-
252
- let builtUser = await buildUser(user, opts, tenantId, dbUser)
253
- // don't allow a user to update its own roles/perms
254
- if (opts.currentUserId && opts.currentUserId === dbUser?._id) {
255
- builtUser.builder = dbUser.builder
256
- builtUser.admin = dbUser.admin
257
- builtUser.roles = dbUser.roles
258
- }
259
-
260
- if (!dbUser && roles?.length) {
261
- builtUser.roles = { ...roles }
262
- }
263
-
264
- // make sure we set the _id field for a new user
265
- // Also if this is a new user, associate groups with them
266
- let groupPromises = []
267
- if (!_id) {
268
- _id = builtUser._id!
269
-
270
- if (userGroups.length > 0) {
271
- for (let groupId of userGroups) {
272
- groupPromises.push(pro.groups.addUsers(groupId, [_id]))
273
- }
274
- }
275
- }
276
-
277
- try {
278
- // save the user to db
279
- let response = await db.put(builtUser)
280
- builtUser._rev = response.rev
281
-
282
- await eventHelpers.handleSaveEvents(builtUser, dbUser)
283
- await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
284
- await cache.user.invalidateUser(response.id)
285
-
286
- await Promise.all(groupPromises)
287
-
288
- // finally returned the saved user from the db
289
- return db.get(builtUser._id!)
290
- } catch (err: any) {
291
- if (err.status === 409) {
292
- throw "User exists already"
293
- } else {
294
- throw err
295
- }
296
- }
297
- })
298
- }
299
-
300
- const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
301
- const lcEmails = emails.map(email => email.toLowerCase())
302
- const params = {
303
- keys: lcEmails,
304
- include_docs: true,
305
- }
306
-
307
- const opts = {
308
- arrayResponse: true,
309
- }
310
-
311
- return dbUtils.queryGlobalView(
312
- ViewName.USER_BY_EMAIL,
313
- params,
314
- undefined,
315
- opts
316
- ) as Promise<User[]>
317
- }
318
-
319
- const getExistingPlatformUsers = async (
320
- emails: string[]
321
- ): Promise<PlatformUserByEmail[]> => {
322
- const lcEmails = emails.map(email => email.toLowerCase())
323
- const params = {
324
- keys: lcEmails,
325
- include_docs: true,
326
- }
327
-
328
- const opts = {
329
- arrayResponse: true,
330
- }
331
- return dbUtils.queryPlatformView(
332
- ViewName.PLATFORM_USERS_LOWERCASE,
333
- params,
334
- opts
335
- ) as Promise<PlatformUserByEmail[]>
336
- }
337
-
338
- const getExistingAccounts = async (
339
- emails: string[]
340
- ): Promise<AccountMetadata[]> => {
341
- const lcEmails = emails.map(email => email.toLowerCase())
342
- const params = {
343
- keys: lcEmails,
344
- include_docs: true,
345
- }
346
-
347
- const opts = {
348
- arrayResponse: true,
349
- }
350
-
351
- return dbUtils.queryPlatformView(
352
- ViewName.ACCOUNT_BY_EMAIL,
353
- params,
354
- opts
355
- ) as Promise<AccountMetadata[]>
356
- }
357
-
358
- /**
359
- * Apply a system-wide search on emails:
360
- * - in tenant
361
- * - cross tenant
362
- * - accounts
363
- * return an array of emails that match the supplied emails.
364
- */
365
- const searchExistingEmails = async (emails: string[]) => {
366
- let matchedEmails: string[] = []
367
-
368
- const existingTenantUsers = await getExistingTenantUsers(emails)
369
- matchedEmails.push(...existingTenantUsers.map(user => user.email))
370
-
371
- const existingPlatformUsers = await getExistingPlatformUsers(emails)
372
- matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
373
-
374
- const existingAccounts = await getExistingAccounts(emails)
375
- matchedEmails.push(...existingAccounts.map(account => account.email))
376
-
377
- return [...new Set(matchedEmails.map(email => email.toLowerCase()))]
378
- }
379
-
380
- export const bulkCreate = async (
381
- newUsersRequested: User[],
382
- groups: string[]
383
- ): Promise<BulkUserCreated> => {
384
- const tenantId = tenancy.getTenantId()
385
-
386
- let usersToSave: any[] = []
387
- let newUsers: any[] = []
388
-
389
- const emails = newUsersRequested.map((user: User) => user.email)
390
- const existingEmails = await searchExistingEmails(emails)
391
- const unsuccessful: { email: string; reason: string }[] = []
392
-
393
- for (const newUser of newUsersRequested) {
394
- if (
395
- newUsers.find(
396
- (x: User) => x.email.toLowerCase() === newUser.email.toLowerCase()
397
- ) ||
398
- existingEmails.includes(newUser.email.toLowerCase())
399
- ) {
400
- unsuccessful.push({
401
- email: newUser.email,
402
- reason: `Unavailable`,
403
- })
404
- continue
405
- }
406
- newUser.userGroups = groups
407
- newUsers.push(newUser)
408
- }
409
-
410
- const account = await accountSdk.api.getAccountByTenantId(tenantId)
411
- return pro.quotas.addUsers(newUsers.length, async () => {
412
- // create the promises array that will be called by bulkDocs
413
- newUsers.forEach((user: any) => {
414
- usersToSave.push(
415
- buildUser(
416
- user,
417
- {
418
- hashPassword: true,
419
- requirePassword: user.requirePassword,
420
- },
421
- tenantId,
422
- undefined, // no dbUser
423
- account
424
- )
425
- )
426
- })
427
-
428
- const usersToBulkSave = await Promise.all(usersToSave)
429
- await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
430
-
431
- // Post-processing of bulk added users, e.g. events and cache operations
432
- for (const user of usersToBulkSave) {
433
- // TODO: Refactor to bulk insert users into the info db
434
- // instead of relying on looping tenant creation
435
- await platform.users.addUser(tenantId, user._id, user.email)
436
- await eventHelpers.handleSaveEvents(user, undefined)
437
- }
438
-
439
- const saved = usersToBulkSave.map(user => {
440
- return {
441
- _id: user._id,
442
- email: user.email,
443
- }
444
- })
445
-
446
- // now update the groups
447
- if (Array.isArray(saved) && groups) {
448
- const groupPromises = []
449
- const createdUserIds = saved.map(user => user._id)
450
- for (let groupId of groups) {
451
- groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
452
- }
453
- await Promise.all(groupPromises)
454
- }
455
-
456
- return {
457
- successful: saved,
458
- unsuccessful,
459
- }
460
- })
461
- }
462
-
463
- /**
464
- * For the given user id's, return the account holder if it is in the ids.
465
- */
466
- const getAccountHolderFromUserIds = async (
467
- userIds: string[]
468
- ): Promise<CloudAccount | undefined> => {
469
- if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
470
- const tenantId = tenancy.getTenantId()
471
- const account = await accounts.getAccountByTenantId(tenantId)
472
- if (!account) {
473
- throw new Error(`Account not found for tenantId=${tenantId}`)
474
- }
475
-
476
- const budibaseUserId = account.budibaseUserId
477
- if (userIds.includes(budibaseUserId)) {
478
- return account
479
- }
480
- }
481
- }
482
-
483
- export const bulkDelete = async (
484
- userIds: string[]
485
- ): Promise<BulkUserDeleted> => {
486
- const db = tenancy.getGlobalDB()
487
-
488
- const response: BulkUserDeleted = {
489
- successful: [],
490
- unsuccessful: [],
491
- }
492
-
493
- // remove the account holder from the delete request if present
494
- const account = await getAccountHolderFromUserIds(userIds)
495
- if (account) {
496
- userIds = userIds.filter(u => u !== account.budibaseUserId)
497
- // mark user as unsuccessful
498
- response.unsuccessful.push({
499
- _id: account.budibaseUserId,
500
- email: account.email,
501
- reason: "Account holder cannot be deleted",
502
- })
503
- }
504
-
505
- // Get users and delete
506
- const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
507
- include_docs: true,
508
- keys: userIds,
509
- })
510
- const usersToDelete: User[] = allDocsResponse.rows.map(
511
- (user: RowResponse<User>) => {
512
- return user.doc
513
- }
514
- )
515
-
516
- // Delete from DB
517
- const toDelete = usersToDelete.map(user => ({
518
- ...user,
519
- _deleted: true,
520
- }))
521
- const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
522
-
523
- await pro.quotas.removeUsers(toDelete.length)
524
- for (let user of usersToDelete) {
525
- await bulkDeleteProcessing(user)
526
- }
527
-
528
- // Build Response
529
- // index users by id
530
- const userIndex: { [key: string]: User } = {}
531
- usersToDelete.reduce((prev, current) => {
532
- prev[current._id!] = current
533
- return prev
534
- }, userIndex)
535
-
536
- // add the successful and unsuccessful users to response
537
- dbResponse.forEach(item => {
538
- const email = userIndex[item.id].email
539
- if (item.ok) {
540
- response.successful.push({ _id: item.id, email })
541
- } else {
542
- response.unsuccessful.push({
543
- _id: item.id,
544
- email,
545
- reason: "Database error",
546
- })
547
- }
548
- })
549
-
550
- return response
551
- }
552
-
553
- // TODO: The single delete should re-use the bulk delete with a single
554
- // user so that we don't need to duplicate logic
555
- export const destroy = async (id: string) => {
556
- const db = tenancy.getGlobalDB()
557
- const dbUser = (await db.get(id)) as User
558
- const userId = dbUser._id as string
559
-
560
- if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
561
- // root account holder can't be deleted from inside budibase
562
- const email = dbUser.email
563
- const account = await accounts.getAccount(email)
564
- if (account) {
565
- if (dbUser.userId === context.getIdentity()!._id) {
566
- throw new HTTPError('Please visit "Account" to delete this user', 400)
567
- } else {
568
- throw new HTTPError("Account holder cannot be deleted", 400)
569
- }
570
- }
571
- }
572
-
573
- await platform.users.removeUser(dbUser)
574
-
575
- await db.remove(userId, dbUser._rev)
576
-
577
- await pro.quotas.removeUsers(1)
578
- await eventHelpers.handleDeleteEvents(dbUser)
579
- await cache.user.invalidateUser(userId)
580
- await sessions.invalidateSessions(userId, { reason: "deletion" })
581
- }
582
-
583
- const bulkDeleteProcessing = async (dbUser: User) => {
584
- const userId = dbUser._id as string
585
- await platform.users.removeUser(dbUser)
586
- await eventHelpers.handleDeleteEvents(dbUser)
587
- await cache.user.invalidateUser(userId)
588
- await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })
589
- }
590
-
591
- export const invite = async (
6
+ export async function invite(
592
7
  users: InviteUsersRequest
593
- ): Promise<InviteUsersResponse> => {
8
+ ): Promise<InviteUsersResponse> {
594
9
  const response: InviteUsersResponse = {
595
10
  successful: [],
596
11
  unsuccessful: [],
597
12
  }
598
13
 
599
- const matchedEmails = await searchExistingEmails(users.map(u => u.email))
14
+ const matchedEmails = await usersCore.searchExistingEmails(
15
+ users.map(u => u.email)
16
+ )
600
17
  const newUsers = []
601
18
 
602
19
  // separate duplicates from new users
@@ -251,9 +251,9 @@ class TestConfiguration {
251
251
  })
252
252
  }
253
253
 
254
- async getUser(email: string): Promise<User | undefined> {
255
- return context.doInTenant(this.getTenantId(), () => {
256
- return users.getGlobalUserByEmail(email)
254
+ async getUser(email: string): Promise<User> {
255
+ return context.doInTenant(this.getTenantId(), async () => {
256
+ return (await users.getGlobalUserByEmail(email)) as User
257
257
  })
258
258
  }
259
259
 
@@ -263,7 +263,7 @@ class TestConfiguration {
263
263
  }
264
264
  const response = await this._req(user, null, controllers.users.save)
265
265
  const body = response as SaveUserResponse
266
- return this.getUser(body.email) as Promise<User>
266
+ return (await this.getUser(body.email)) as User
267
267
  }
268
268
 
269
269
  // CONFIGS
@@ -140,4 +140,24 @@ export class UserAPI extends TestAPI {
140
140
  .expect("Content-Type", /json/)
141
141
  .expect(opts?.status ? opts.status : 200)
142
142
  }
143
+
144
+ grantBuilderToApp = (
145
+ userId: string,
146
+ appId: string,
147
+ statusCode: number = 200
148
+ ) => {
149
+ return this.request
150
+ .post(`/api/global/users/${userId}/app/${appId}/builder`)
151
+ .set(this.config.defaultHeaders())
152
+ .expect("Content-Type", /json/)
153
+ .expect(statusCode)
154
+ }
155
+
156
+ revokeBuilderFromApp = (userId: string, appId: string) => {
157
+ return this.request
158
+ .delete(`/api/global/users/${userId}/app/${appId}/builder`)
159
+ .set(this.config.defaultHeaders())
160
+ .expect("Content-Type", /json/)
161
+ .expect(200)
162
+ }
143
163
  }
@@ -2,7 +2,7 @@ import * as email from "./email"
2
2
  import { mocks } from "@budibase/backend-core/tests"
3
3
 
4
4
  import * as _pro from "@budibase/pro"
5
- const pro = jest.mocked(_pro, true)
5
+ const pro = jest.mocked(_pro, { shallow: false })
6
6
 
7
7
  export default {
8
8
  email,