@budibase/backend-core 2.9.40-alpha.6 → 2.10.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 (252) hide show
  1. package/dist/index.js +5 -4
  2. package/dist/index.js.map +2 -2
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +6 -6
  5. package/dist/src/cache/appMetadata.js +1 -1
  6. package/dist/src/cache/appMetadata.js.map +1 -1
  7. package/dist/src/constants/misc.d.ts +0 -2
  8. package/dist/src/constants/misc.js +0 -2
  9. package/dist/src/constants/misc.js.map +1 -1
  10. package/dist/src/environment.js +5 -4
  11. package/dist/src/environment.js.map +1 -1
  12. package/dist/src/logging/system.d.ts +1 -1
  13. package/dist/src/timers/timers.d.ts +1 -1
  14. package/package.json +6 -6
  15. package/src/accounts/accounts.ts +82 -0
  16. package/src/accounts/api.ts +59 -0
  17. package/src/accounts/index.ts +1 -0
  18. package/src/auth/auth.ts +208 -0
  19. package/src/auth/index.ts +1 -0
  20. package/src/auth/tests/auth.spec.ts +14 -0
  21. package/src/blacklist/blacklist.ts +54 -0
  22. package/src/blacklist/index.ts +1 -0
  23. package/src/blacklist/tests/blacklist.spec.ts +46 -0
  24. package/src/cache/appMetadata.ts +88 -0
  25. package/src/cache/base/index.ts +92 -0
  26. package/src/cache/generic.ts +30 -0
  27. package/src/cache/index.ts +5 -0
  28. package/src/cache/tests/writethrough.spec.ts +138 -0
  29. package/src/cache/user.ts +83 -0
  30. package/src/cache/writethrough.ts +133 -0
  31. package/src/configs/configs.ts +257 -0
  32. package/src/configs/index.ts +1 -0
  33. package/src/configs/tests/configs.spec.ts +184 -0
  34. package/src/constants/db.ts +63 -0
  35. package/src/constants/index.ts +2 -0
  36. package/src/constants/misc.ts +50 -0
  37. package/src/context/Context.ts +14 -0
  38. package/src/context/identity.ts +58 -0
  39. package/src/context/index.ts +3 -0
  40. package/src/context/mainContext.ts +310 -0
  41. package/src/context/tests/index.spec.ts +147 -0
  42. package/src/context/types.ts +11 -0
  43. package/src/db/Replication.ts +84 -0
  44. package/src/db/constants.ts +10 -0
  45. package/src/db/couch/DatabaseImpl.ts +238 -0
  46. package/src/db/couch/connections.ts +77 -0
  47. package/src/db/couch/index.ts +5 -0
  48. package/src/db/couch/pouchDB.ts +97 -0
  49. package/src/db/couch/pouchDump.ts +0 -0
  50. package/src/db/couch/utils.ts +50 -0
  51. package/src/db/db.ts +43 -0
  52. package/src/db/errors.ts +14 -0
  53. package/src/db/index.ts +12 -0
  54. package/src/db/lucene.ts +750 -0
  55. package/src/db/searchIndexes/index.ts +1 -0
  56. package/src/db/searchIndexes/searchIndexes.ts +62 -0
  57. package/src/db/tests/index.spec.js +25 -0
  58. package/src/db/tests/lucene.spec.ts +368 -0
  59. package/src/db/tests/pouch.spec.js +62 -0
  60. package/src/db/tests/utils.spec.ts +63 -0
  61. package/src/db/utils.ts +207 -0
  62. package/src/db/views.ts +241 -0
  63. package/src/docIds/conversions.ts +59 -0
  64. package/src/docIds/ids.ts +113 -0
  65. package/src/docIds/index.ts +2 -0
  66. package/src/docIds/newid.ts +5 -0
  67. package/src/docIds/params.ts +174 -0
  68. package/src/docUpdates/index.ts +29 -0
  69. package/src/environment.ts +201 -0
  70. package/src/errors/errors.ts +119 -0
  71. package/src/errors/index.ts +1 -0
  72. package/src/events/analytics.ts +6 -0
  73. package/src/events/asyncEvents/index.ts +2 -0
  74. package/src/events/asyncEvents/publisher.ts +12 -0
  75. package/src/events/asyncEvents/queue.ts +22 -0
  76. package/src/events/backfill.ts +183 -0
  77. package/src/events/documentId.ts +56 -0
  78. package/src/events/events.ts +40 -0
  79. package/src/events/identification.ts +310 -0
  80. package/src/events/index.ts +14 -0
  81. package/src/events/processors/AnalyticsProcessor.ts +64 -0
  82. package/src/events/processors/AuditLogsProcessor.ts +93 -0
  83. package/src/events/processors/LoggingProcessor.ts +37 -0
  84. package/src/events/processors/Processors.ts +52 -0
  85. package/src/events/processors/async/DocumentUpdateProcessor.ts +43 -0
  86. package/src/events/processors/index.ts +19 -0
  87. package/src/events/processors/posthog/PosthogProcessor.ts +118 -0
  88. package/src/events/processors/posthog/index.ts +2 -0
  89. package/src/events/processors/posthog/rateLimiting.ts +106 -0
  90. package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +168 -0
  91. package/src/events/processors/types.ts +1 -0
  92. package/src/events/publishers/account.ts +35 -0
  93. package/src/events/publishers/app.ts +155 -0
  94. package/src/events/publishers/auditLog.ts +26 -0
  95. package/src/events/publishers/auth.ts +73 -0
  96. package/src/events/publishers/automation.ts +110 -0
  97. package/src/events/publishers/backfill.ts +74 -0
  98. package/src/events/publishers/backup.ts +42 -0
  99. package/src/events/publishers/datasource.ts +48 -0
  100. package/src/events/publishers/email.ts +17 -0
  101. package/src/events/publishers/environmentVariable.ts +38 -0
  102. package/src/events/publishers/group.ts +99 -0
  103. package/src/events/publishers/index.ts +24 -0
  104. package/src/events/publishers/installation.ts +38 -0
  105. package/src/events/publishers/layout.ts +26 -0
  106. package/src/events/publishers/license.ts +84 -0
  107. package/src/events/publishers/org.ts +37 -0
  108. package/src/events/publishers/plugin.ts +47 -0
  109. package/src/events/publishers/query.ts +88 -0
  110. package/src/events/publishers/role.ts +62 -0
  111. package/src/events/publishers/rows.ts +29 -0
  112. package/src/events/publishers/screen.ts +36 -0
  113. package/src/events/publishers/serve.ts +43 -0
  114. package/src/events/publishers/table.ts +70 -0
  115. package/src/events/publishers/user.ts +202 -0
  116. package/src/events/publishers/view.ts +107 -0
  117. package/src/features/index.ts +78 -0
  118. package/src/features/installation.ts +17 -0
  119. package/src/features/tests/featureFlags.spec.ts +85 -0
  120. package/src/helpers.ts +9 -0
  121. package/src/index.ts +54 -0
  122. package/src/installation.ts +107 -0
  123. package/src/logging/alerts.ts +26 -0
  124. package/src/logging/correlation/correlation.ts +13 -0
  125. package/src/logging/correlation/index.ts +1 -0
  126. package/src/logging/correlation/middleware.ts +17 -0
  127. package/src/logging/index.ts +4 -0
  128. package/src/logging/pino/logger.ts +232 -0
  129. package/src/logging/pino/middleware.ts +45 -0
  130. package/src/logging/system.ts +81 -0
  131. package/src/logging/tests/system.spec.ts +61 -0
  132. package/src/middleware/adminOnly.ts +9 -0
  133. package/src/middleware/auditLog.ts +6 -0
  134. package/src/middleware/authenticated.ts +193 -0
  135. package/src/middleware/builderOnly.ts +21 -0
  136. package/src/middleware/builderOrAdmin.ts +21 -0
  137. package/src/middleware/csrf.ts +81 -0
  138. package/src/middleware/errorHandling.ts +29 -0
  139. package/src/middleware/index.ts +21 -0
  140. package/src/middleware/internalApi.ts +23 -0
  141. package/src/middleware/joi-validator.ts +45 -0
  142. package/src/middleware/matchers.ts +47 -0
  143. package/src/middleware/passport/datasource/google.ts +95 -0
  144. package/src/middleware/passport/local.ts +54 -0
  145. package/src/middleware/passport/sso/google.ts +77 -0
  146. package/src/middleware/passport/sso/oidc.ts +154 -0
  147. package/src/middleware/passport/sso/sso.ts +165 -0
  148. package/src/middleware/passport/sso/tests/google.spec.ts +67 -0
  149. package/src/middleware/passport/sso/tests/oidc.spec.ts +152 -0
  150. package/src/middleware/passport/sso/tests/sso.spec.ts +197 -0
  151. package/src/middleware/passport/utils.ts +38 -0
  152. package/src/middleware/querystringToBody.ts +28 -0
  153. package/src/middleware/tenancy.ts +36 -0
  154. package/src/middleware/tests/builder.spec.ts +180 -0
  155. package/src/middleware/tests/matchers.spec.ts +134 -0
  156. package/src/migrations/definitions.ts +40 -0
  157. package/src/migrations/index.ts +2 -0
  158. package/src/migrations/migrations.ts +191 -0
  159. package/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +11 -0
  160. package/src/migrations/tests/migrations.spec.ts +64 -0
  161. package/src/objectStore/buckets/app.ts +40 -0
  162. package/src/objectStore/buckets/global.ts +29 -0
  163. package/src/objectStore/buckets/index.ts +3 -0
  164. package/src/objectStore/buckets/plugins.ts +71 -0
  165. package/src/objectStore/buckets/tests/app.spec.ts +171 -0
  166. package/src/objectStore/buckets/tests/global.spec.ts +74 -0
  167. package/src/objectStore/buckets/tests/plugins.spec.ts +111 -0
  168. package/src/objectStore/cloudfront.ts +41 -0
  169. package/src/objectStore/index.ts +3 -0
  170. package/src/objectStore/objectStore.ts +440 -0
  171. package/src/objectStore/utils.ts +27 -0
  172. package/src/platform/index.ts +3 -0
  173. package/src/platform/platformDb.ts +6 -0
  174. package/src/platform/tenants.ts +101 -0
  175. package/src/platform/tests/tenants.spec.ts +26 -0
  176. package/src/platform/users.ts +90 -0
  177. package/src/plugin/index.ts +1 -0
  178. package/src/plugin/tests/validation.spec.ts +83 -0
  179. package/src/plugin/utils.ts +156 -0
  180. package/src/queue/constants.ts +6 -0
  181. package/src/queue/inMemoryQueue.ts +141 -0
  182. package/src/queue/index.ts +2 -0
  183. package/src/queue/listeners.ts +195 -0
  184. package/src/queue/queue.ts +54 -0
  185. package/src/redis/index.ts +6 -0
  186. package/src/redis/init.ts +86 -0
  187. package/src/redis/redis.ts +308 -0
  188. package/src/redis/redlockImpl.ts +139 -0
  189. package/src/redis/utils.ts +117 -0
  190. package/src/security/encryption.ts +179 -0
  191. package/src/security/permissions.ts +158 -0
  192. package/src/security/roles.ts +389 -0
  193. package/src/security/sessions.ts +120 -0
  194. package/src/security/tests/encryption.spec.ts +31 -0
  195. package/src/security/tests/permissions.spec.ts +145 -0
  196. package/src/security/tests/sessions.spec.ts +12 -0
  197. package/src/tenancy/db.ts +6 -0
  198. package/src/tenancy/index.ts +2 -0
  199. package/src/tenancy/tenancy.ts +140 -0
  200. package/src/tenancy/tests/tenancy.spec.ts +184 -0
  201. package/src/timers/index.ts +1 -0
  202. package/src/timers/timers.ts +22 -0
  203. package/src/users/db.ts +484 -0
  204. package/src/users/events.ts +176 -0
  205. package/src/users/index.ts +4 -0
  206. package/src/users/lookup.ts +102 -0
  207. package/src/users/users.ts +276 -0
  208. package/src/users/utils.ts +55 -0
  209. package/src/utils/hashing.ts +14 -0
  210. package/src/utils/index.ts +3 -0
  211. package/src/utils/stringUtils.ts +8 -0
  212. package/src/utils/tests/utils.spec.ts +191 -0
  213. package/src/utils/utils.ts +239 -0
  214. package/tests/core/logging.ts +34 -0
  215. package/tests/core/utilities/index.ts +6 -0
  216. package/tests/core/utilities/jestUtils.ts +30 -0
  217. package/tests/core/utilities/mocks/alerts.ts +3 -0
  218. package/tests/core/utilities/mocks/date.ts +2 -0
  219. package/tests/core/utilities/mocks/events.ts +131 -0
  220. package/tests/core/utilities/mocks/fetch.ts +17 -0
  221. package/tests/core/utilities/mocks/index.ts +10 -0
  222. package/tests/core/utilities/mocks/licenses.ts +115 -0
  223. package/tests/core/utilities/mocks/posthog.ts +7 -0
  224. package/tests/core/utilities/structures/Chance.ts +20 -0
  225. package/tests/core/utilities/structures/accounts.ts +115 -0
  226. package/tests/core/utilities/structures/apps.ts +21 -0
  227. package/tests/core/utilities/structures/common.ts +7 -0
  228. package/tests/core/utilities/structures/db.ts +12 -0
  229. package/tests/core/utilities/structures/documents/index.ts +1 -0
  230. package/tests/core/utilities/structures/documents/platform/index.ts +1 -0
  231. package/tests/core/utilities/structures/documents/platform/installation.ts +12 -0
  232. package/tests/core/utilities/structures/generator.ts +2 -0
  233. package/tests/core/utilities/structures/index.ts +15 -0
  234. package/tests/core/utilities/structures/koa.ts +16 -0
  235. package/tests/core/utilities/structures/licenses.ts +167 -0
  236. package/tests/core/utilities/structures/plugins.ts +19 -0
  237. package/tests/core/utilities/structures/quotas.ts +67 -0
  238. package/tests/core/utilities/structures/scim.ts +80 -0
  239. package/tests/core/utilities/structures/shared.ts +19 -0
  240. package/tests/core/utilities/structures/sso.ts +119 -0
  241. package/tests/core/utilities/structures/tenants.ts +5 -0
  242. package/tests/core/utilities/structures/userGroups.ts +10 -0
  243. package/tests/core/utilities/structures/users.ts +73 -0
  244. package/tests/core/utilities/testContainerUtils.ts +85 -0
  245. package/tests/core/utilities/utils/index.ts +1 -0
  246. package/tests/core/utilities/utils/time.ts +3 -0
  247. package/tests/extra/DBTestConfiguration.ts +36 -0
  248. package/tests/extra/index.ts +2 -0
  249. package/tests/extra/testEnv.ts +95 -0
  250. package/tests/index.ts +1 -0
  251. package/tests/jestEnv.ts +6 -0
  252. package/tests/jestSetup.ts +28 -0
@@ -0,0 +1,484 @@
1
+ import env from "../environment"
2
+ import * as eventHelpers from "./events"
3
+ import * as accounts from "../accounts"
4
+ import * as accountSdk from "../accounts"
5
+ import * as cache from "../cache"
6
+ import { getGlobalDB, getIdentity, getTenantId } from "../context"
7
+ import * as dbUtils from "../db"
8
+ import { EmailUnavailableError, HTTPError } from "../errors"
9
+ import * as platform from "../platform"
10
+ import * as sessions from "../security/sessions"
11
+ import * as usersCore from "./users"
12
+ import {
13
+ Account,
14
+ AllDocsResponse,
15
+ BulkUserCreated,
16
+ BulkUserDeleted,
17
+ isSSOAccount,
18
+ isSSOUser,
19
+ RowResponse,
20
+ SaveUserOpts,
21
+ User,
22
+ UserStatus,
23
+ UserGroup,
24
+ ContextUser,
25
+ } from "@budibase/types"
26
+ import {
27
+ getAccountHolderFromUserIds,
28
+ isAdmin,
29
+ validateUniqueUser,
30
+ } from "./utils"
31
+ import { searchExistingEmails } from "./lookup"
32
+ import { hash } from "../utils"
33
+
34
+ type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
35
+ type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
36
+ type FeatureFn = () => Promise<Boolean>
37
+ type GroupGetFn = (ids: string[]) => Promise<UserGroup[]>
38
+ type GroupBuildersFn = (user: User) => Promise<string[]>
39
+ type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
40
+ type GroupFns = {
41
+ addUsers: GroupUpdateFn
42
+ getBulk: GroupGetFn
43
+ getGroupBuilderAppIds: GroupBuildersFn
44
+ }
45
+ type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
46
+
47
+ const bulkDeleteProcessing = async (dbUser: User) => {
48
+ const userId = dbUser._id as string
49
+ await platform.users.removeUser(dbUser)
50
+ await eventHelpers.handleDeleteEvents(dbUser)
51
+ await cache.user.invalidateUser(userId)
52
+ await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })
53
+ }
54
+
55
+ export class UserDB {
56
+ static quotas: QuotaFns
57
+ static groups: GroupFns
58
+ static features: FeatureFns
59
+
60
+ static init(quotaFns: QuotaFns, groupFns: GroupFns, featureFns: FeatureFns) {
61
+ UserDB.quotas = quotaFns
62
+ UserDB.groups = groupFns
63
+ UserDB.features = featureFns
64
+ }
65
+
66
+ static async isPreventPasswordActions(user: User, account?: Account) {
67
+ // when in maintenance mode we allow sso users with the admin role
68
+ // to perform any password action - this prevents lockout
69
+ if (env.ENABLE_SSO_MAINTENANCE_MODE && isAdmin(user)) {
70
+ return false
71
+ }
72
+
73
+ // SSO is enforced for all users
74
+ if (await UserDB.features.isSSOEnforced()) {
75
+ return true
76
+ }
77
+
78
+ // Check local sso
79
+ if (isSSOUser(user)) {
80
+ return true
81
+ }
82
+
83
+ // Check account sso
84
+ if (!account) {
85
+ account = await accountSdk.getAccountByTenantId(getTenantId())
86
+ }
87
+ return !!(account && account.email === user.email && isSSOAccount(account))
88
+ }
89
+
90
+ static async buildUser(
91
+ user: User,
92
+ opts: SaveUserOpts = {
93
+ hashPassword: true,
94
+ requirePassword: true,
95
+ },
96
+ tenantId: string,
97
+ dbUser?: any,
98
+ account?: Account
99
+ ): Promise<User> {
100
+ let { password, _id } = user
101
+
102
+ // don't require a password if the db user doesn't already have one
103
+ if (dbUser && !dbUser.password) {
104
+ opts.requirePassword = false
105
+ }
106
+
107
+ let hashedPassword
108
+ if (password) {
109
+ if (await UserDB.isPreventPasswordActions(user, account)) {
110
+ throw new HTTPError("Password change is disabled for this user", 400)
111
+ }
112
+ hashedPassword = opts.hashPassword ? await hash(password) : password
113
+ } else if (dbUser) {
114
+ hashedPassword = dbUser.password
115
+ }
116
+
117
+ // passwords are never required if sso is enforced
118
+ const requirePasswords =
119
+ opts.requirePassword && !(await UserDB.features.isSSOEnforced())
120
+ if (!hashedPassword && requirePasswords) {
121
+ throw "Password must be specified."
122
+ }
123
+
124
+ _id = _id || dbUtils.generateGlobalUserID()
125
+
126
+ const fullUser = {
127
+ createdAt: Date.now(),
128
+ ...dbUser,
129
+ ...user,
130
+ _id,
131
+ password: hashedPassword,
132
+ tenantId,
133
+ }
134
+ // make sure the roles object is always present
135
+ if (!fullUser.roles) {
136
+ fullUser.roles = {}
137
+ }
138
+ // add the active status to a user if its not provided
139
+ if (fullUser.status == null) {
140
+ fullUser.status = UserStatus.ACTIVE
141
+ }
142
+
143
+ return fullUser
144
+ }
145
+
146
+ static async allUsers() {
147
+ const db = getGlobalDB()
148
+ const response = await db.allDocs(
149
+ dbUtils.getGlobalUserParams(null, {
150
+ include_docs: true,
151
+ })
152
+ )
153
+ return response.rows.map((row: any) => row.doc)
154
+ }
155
+
156
+ static async countUsersByApp(appId: string) {
157
+ let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
158
+ return {
159
+ userCount: response.length,
160
+ }
161
+ }
162
+
163
+ static async getUsersByAppAccess(appId?: string) {
164
+ const opts: any = {
165
+ include_docs: true,
166
+ limit: 50,
167
+ }
168
+ let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
169
+ appId,
170
+ opts
171
+ )
172
+ return response
173
+ }
174
+
175
+ static async getUserByEmail(email: string) {
176
+ return usersCore.getGlobalUserByEmail(email)
177
+ }
178
+
179
+ /**
180
+ * Gets a user by ID from the global database, based on the current tenancy.
181
+ */
182
+ static async getUser(userId: string) {
183
+ const user = await usersCore.getById(userId)
184
+ if (user) {
185
+ delete user.password
186
+ }
187
+ return user
188
+ }
189
+
190
+ static async bulkGet(userIds: string[]) {
191
+ return await usersCore.bulkGetGlobalUsersById(userIds)
192
+ }
193
+
194
+ static async bulkUpdate(users: User[]) {
195
+ return await usersCore.bulkUpdateGlobalUsers(users)
196
+ }
197
+
198
+ static async save(user: User, opts: SaveUserOpts = {}): Promise<User> {
199
+ // default booleans to true
200
+ if (opts.hashPassword == null) {
201
+ opts.hashPassword = true
202
+ }
203
+ if (opts.requirePassword == null) {
204
+ opts.requirePassword = true
205
+ }
206
+ const tenantId = getTenantId()
207
+ const db = getGlobalDB()
208
+
209
+ let { email, _id, userGroups = [], roles } = user
210
+
211
+ if (!email && !_id) {
212
+ throw new Error("_id or email is required")
213
+ }
214
+
215
+ if (
216
+ user.builder?.apps?.length &&
217
+ !(await UserDB.features.isAppBuildersEnabled())
218
+ ) {
219
+ throw new Error("Unable to update app builders, please check license")
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 UserDB.quotas.addUsers(change, async () => {
250
+ await validateUniqueUser(email, tenantId)
251
+
252
+ let builtUser = await UserDB.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 = usersCore.cleanseUserObject(builtUser, dbUser) as User
256
+ }
257
+
258
+ if (!dbUser && roles?.length) {
259
+ builtUser.roles = { ...roles }
260
+ }
261
+
262
+ // make sure we set the _id field for a new user
263
+ // Also if this is a new user, associate groups with them
264
+ let groupPromises = []
265
+ if (!_id) {
266
+ _id = builtUser._id!
267
+
268
+ if (userGroups.length > 0) {
269
+ for (let groupId of userGroups) {
270
+ groupPromises.push(UserDB.groups.addUsers(groupId, [_id!]))
271
+ }
272
+ }
273
+ }
274
+
275
+ try {
276
+ // save the user to db
277
+ let response = await db.put(builtUser)
278
+ builtUser._rev = response.rev
279
+
280
+ await eventHelpers.handleSaveEvents(builtUser, dbUser)
281
+ await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
282
+ await cache.user.invalidateUser(response.id)
283
+
284
+ await Promise.all(groupPromises)
285
+
286
+ // finally returned the saved user from the db
287
+ return db.get(builtUser._id!)
288
+ } catch (err: any) {
289
+ if (err.status === 409) {
290
+ throw "User exists already"
291
+ } else {
292
+ throw err
293
+ }
294
+ }
295
+ })
296
+ }
297
+
298
+ static async bulkCreate(
299
+ newUsersRequested: User[],
300
+ groups: string[]
301
+ ): Promise<BulkUserCreated> {
302
+ const tenantId = getTenantId()
303
+
304
+ let usersToSave: any[] = []
305
+ let newUsers: any[] = []
306
+
307
+ const emails = newUsersRequested.map((user: User) => user.email)
308
+ const existingEmails = await searchExistingEmails(emails)
309
+ const unsuccessful: { email: string; reason: string }[] = []
310
+
311
+ for (const newUser of newUsersRequested) {
312
+ if (
313
+ newUsers.find(
314
+ (x: User) => x.email.toLowerCase() === newUser.email.toLowerCase()
315
+ ) ||
316
+ existingEmails.includes(newUser.email.toLowerCase())
317
+ ) {
318
+ unsuccessful.push({
319
+ email: newUser.email,
320
+ reason: `Unavailable`,
321
+ })
322
+ continue
323
+ }
324
+ newUser.userGroups = groups
325
+ newUsers.push(newUser)
326
+ }
327
+
328
+ const account = await accountSdk.getAccountByTenantId(tenantId)
329
+ return UserDB.quotas.addUsers(newUsers.length, async () => {
330
+ // create the promises array that will be called by bulkDocs
331
+ newUsers.forEach((user: any) => {
332
+ usersToSave.push(
333
+ UserDB.buildUser(
334
+ user,
335
+ {
336
+ hashPassword: true,
337
+ requirePassword: user.requirePassword,
338
+ },
339
+ tenantId,
340
+ undefined, // no dbUser
341
+ account
342
+ )
343
+ )
344
+ })
345
+
346
+ const usersToBulkSave = await Promise.all(usersToSave)
347
+ await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
348
+
349
+ // Post-processing of bulk added users, e.g. events and cache operations
350
+ for (const user of usersToBulkSave) {
351
+ // TODO: Refactor to bulk insert users into the info db
352
+ // instead of relying on looping tenant creation
353
+ await platform.users.addUser(tenantId, user._id, user.email)
354
+ await eventHelpers.handleSaveEvents(user, undefined)
355
+ }
356
+
357
+ const saved = usersToBulkSave.map(user => {
358
+ return {
359
+ _id: user._id,
360
+ email: user.email,
361
+ }
362
+ })
363
+
364
+ // now update the groups
365
+ if (Array.isArray(saved) && groups) {
366
+ const groupPromises = []
367
+ const createdUserIds = saved.map(user => user._id)
368
+ for (let groupId of groups) {
369
+ groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
370
+ }
371
+ await Promise.all(groupPromises)
372
+ }
373
+
374
+ return {
375
+ successful: saved,
376
+ unsuccessful,
377
+ }
378
+ })
379
+ }
380
+
381
+ static async bulkDelete(userIds: string[]): Promise<BulkUserDeleted> {
382
+ const db = getGlobalDB()
383
+
384
+ const response: BulkUserDeleted = {
385
+ successful: [],
386
+ unsuccessful: [],
387
+ }
388
+
389
+ // remove the account holder from the delete request if present
390
+ const account = await getAccountHolderFromUserIds(userIds)
391
+ if (account) {
392
+ userIds = userIds.filter(u => u !== account.budibaseUserId)
393
+ // mark user as unsuccessful
394
+ response.unsuccessful.push({
395
+ _id: account.budibaseUserId,
396
+ email: account.email,
397
+ reason: "Account holder cannot be deleted",
398
+ })
399
+ }
400
+
401
+ // Get users and delete
402
+ const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
403
+ include_docs: true,
404
+ keys: userIds,
405
+ })
406
+ const usersToDelete: User[] = allDocsResponse.rows.map(
407
+ (user: RowResponse<User>) => {
408
+ return user.doc
409
+ }
410
+ )
411
+
412
+ // Delete from DB
413
+ const toDelete = usersToDelete.map(user => ({
414
+ ...user,
415
+ _deleted: true,
416
+ }))
417
+ const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
418
+
419
+ await UserDB.quotas.removeUsers(toDelete.length)
420
+ for (let user of usersToDelete) {
421
+ await bulkDeleteProcessing(user)
422
+ }
423
+
424
+ // Build Response
425
+ // index users by id
426
+ const userIndex: { [key: string]: User } = {}
427
+ usersToDelete.reduce((prev, current) => {
428
+ prev[current._id!] = current
429
+ return prev
430
+ }, userIndex)
431
+
432
+ // add the successful and unsuccessful users to response
433
+ dbResponse.forEach(item => {
434
+ const email = userIndex[item.id].email
435
+ if (item.ok) {
436
+ response.successful.push({ _id: item.id, email })
437
+ } else {
438
+ response.unsuccessful.push({
439
+ _id: item.id,
440
+ email,
441
+ reason: "Database error",
442
+ })
443
+ }
444
+ })
445
+
446
+ return response
447
+ }
448
+
449
+ static async destroy(id: string) {
450
+ const db = getGlobalDB()
451
+ const dbUser = (await db.get(id)) as User
452
+ const userId = dbUser._id as string
453
+
454
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
455
+ // root account holder can't be deleted from inside budibase
456
+ const email = dbUser.email
457
+ const account = await accounts.getAccount(email)
458
+ if (account) {
459
+ if (dbUser.userId === getIdentity()!._id) {
460
+ throw new HTTPError('Please visit "Account" to delete this user', 400)
461
+ } else {
462
+ throw new HTTPError("Account holder cannot be deleted", 400)
463
+ }
464
+ }
465
+ }
466
+
467
+ await platform.users.removeUser(dbUser)
468
+
469
+ await db.remove(userId, dbUser._rev)
470
+
471
+ await UserDB.quotas.removeUsers(1)
472
+ await eventHelpers.handleDeleteEvents(dbUser)
473
+ await cache.user.invalidateUser(userId)
474
+ await sessions.invalidateSessions(userId, { reason: "deletion" })
475
+ }
476
+
477
+ static async getGroups(groupIds: string[]) {
478
+ return await this.groups.getBulk(groupIds)
479
+ }
480
+
481
+ static async getGroupBuilderAppIds(user: User) {
482
+ return await this.groups.getGroupBuilderAppIds(user)
483
+ }
484
+ }
@@ -0,0 +1,176 @@
1
+ import env from "../environment"
2
+ import * as events from "../events"
3
+ import * as accounts from "../accounts"
4
+ import { getTenantId } from "../context"
5
+ import { User, UserRoles, CloudAccount } from "@budibase/types"
6
+ import { hasBuilderPermissions, hasAdminPermissions } from "./utils"
7
+
8
+ export const handleDeleteEvents = async (user: any) => {
9
+ await events.user.deleted(user)
10
+
11
+ if (hasBuilderPermissions(user)) {
12
+ await events.user.permissionBuilderRemoved(user)
13
+ }
14
+
15
+ if (hasAdminPermissions(user)) {
16
+ await events.user.permissionAdminRemoved(user)
17
+ }
18
+ }
19
+
20
+ const assignAppRoleEvents = async (
21
+ user: User,
22
+ roles: UserRoles,
23
+ existingRoles: UserRoles
24
+ ) => {
25
+ for (const [appId, role] of Object.entries(roles)) {
26
+ // app role in existing is not same as new
27
+ if (!existingRoles || existingRoles[appId] !== role) {
28
+ await events.role.assigned(user, role)
29
+ }
30
+ }
31
+ }
32
+
33
+ const unassignAppRoleEvents = async (
34
+ user: User,
35
+ roles: UserRoles,
36
+ existingRoles: UserRoles
37
+ ) => {
38
+ if (!existingRoles) {
39
+ return
40
+ }
41
+ for (const [appId, role] of Object.entries(existingRoles)) {
42
+ // app role in new is not same as existing
43
+ if (!roles || roles[appId] !== role) {
44
+ await events.role.unassigned(user, role)
45
+ }
46
+ }
47
+ }
48
+
49
+ const handleAppRoleEvents = async (user: any, existingUser: any) => {
50
+ const roles = user.roles
51
+ const existingRoles = existingUser?.roles
52
+
53
+ await assignAppRoleEvents(user, roles, existingRoles)
54
+ await unassignAppRoleEvents(user, roles, existingRoles)
55
+ }
56
+
57
+ export const handleSaveEvents = async (
58
+ user: User,
59
+ existingUser: User | undefined
60
+ ) => {
61
+ const tenantId = getTenantId()
62
+ let tenantAccount: CloudAccount | undefined
63
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
64
+ tenantAccount = await accounts.getAccountByTenantId(tenantId)
65
+ }
66
+ await events.identification.identifyUser(user, tenantAccount)
67
+
68
+ if (existingUser) {
69
+ await events.user.updated(user)
70
+
71
+ if (isRemovingBuilder(user, existingUser)) {
72
+ await events.user.permissionBuilderRemoved(user)
73
+ }
74
+
75
+ if (isRemovingAdmin(user, existingUser)) {
76
+ await events.user.permissionAdminRemoved(user)
77
+ }
78
+
79
+ if (isOnboardingComplete(user, existingUser)) {
80
+ await events.user.onboardingComplete(user)
81
+ }
82
+
83
+ if (
84
+ !existingUser.forceResetPassword &&
85
+ user.forceResetPassword &&
86
+ user.password
87
+ ) {
88
+ await events.user.passwordForceReset(user)
89
+ }
90
+
91
+ if (user.password !== existingUser.password) {
92
+ await events.user.passwordUpdated(user)
93
+ }
94
+ } else {
95
+ await events.user.created(user)
96
+ }
97
+
98
+ if (isAddingBuilder(user, existingUser)) {
99
+ await events.user.permissionBuilderAssigned(user)
100
+ }
101
+
102
+ if (isAddingAdmin(user, existingUser)) {
103
+ await events.user.permissionAdminAssigned(user)
104
+ }
105
+
106
+ await handleAppRoleEvents(user, existingUser)
107
+ }
108
+
109
+ export const isAddingBuilder = (user: any, existingUser: any) => {
110
+ return isAddingPermission(user, existingUser, hasBuilderPermissions)
111
+ }
112
+
113
+ export const isRemovingBuilder = (user: any, existingUser: any) => {
114
+ return isRemovingPermission(user, existingUser, hasBuilderPermissions)
115
+ }
116
+
117
+ const isAddingAdmin = (user: any, existingUser: any) => {
118
+ return isAddingPermission(user, existingUser, hasAdminPermissions)
119
+ }
120
+
121
+ const isRemovingAdmin = (user: any, existingUser: any) => {
122
+ return isRemovingPermission(user, existingUser, hasAdminPermissions)
123
+ }
124
+
125
+ const isOnboardingComplete = (user: any, existingUser: any) => {
126
+ return !existingUser?.onboardedAt && typeof user.onboardedAt === "string"
127
+ }
128
+
129
+ /**
130
+ * Check if a permission is being added to a new or existing user.
131
+ */
132
+ const isAddingPermission = (
133
+ user: any,
134
+ existingUser: any,
135
+ hasPermission: any
136
+ ) => {
137
+ // new user doesn't have the permission
138
+ if (!hasPermission(user)) {
139
+ return false
140
+ }
141
+
142
+ // existing user has the permission
143
+ if (existingUser && hasPermission(existingUser)) {
144
+ return false
145
+ }
146
+
147
+ // permission is being added
148
+ return true
149
+ }
150
+
151
+ /**
152
+ * Check if a permission is being removed from an existing user.
153
+ */
154
+ const isRemovingPermission = (
155
+ user: any,
156
+ existingUser: any,
157
+ hasPermission: any
158
+ ) => {
159
+ // new user has the permission
160
+ if (hasPermission(user)) {
161
+ return false
162
+ }
163
+
164
+ // no existing user or existing user doesn't have the permission
165
+ if (!existingUser) {
166
+ return false
167
+ }
168
+
169
+ // existing user doesn't have the permission
170
+ if (!hasPermission(existingUser)) {
171
+ return false
172
+ }
173
+
174
+ // permission is being removed
175
+ return true
176
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./users"
2
+ export * from "./utils"
3
+ export * from "./lookup"
4
+ export { UserDB } from "./db"