@budibase/worker 2.5.9 → 2.5.10-alpha.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.
- package/package.json +9 -8
- package/scripts/dev/manage.js +1 -0
- package/src/api/controllers/global/users.ts +7 -4
- package/src/api/controllers/system/accounts.ts +2 -2
- package/src/api/index.ts +1 -1
- package/src/api/routes/global/configs.ts +9 -1
- package/src/api/routes/global/tests/scim.spec.ts +1029 -0
- package/src/api/routes/global/tests/users.spec.ts +10 -4
- package/src/api/routes/index.ts +4 -6
- package/src/constants/templates/core.hbs +2 -1
- package/src/environment.ts +0 -1
- package/src/index.ts +7 -7
- package/src/initPro.ts +13 -0
- package/src/middleware/handleScimBody.ts +12 -0
- package/src/sdk/accounts/metadata.ts +11 -2
- package/src/sdk/users/users.ts +101 -135
- package/src/tests/TestConfiguration.ts +50 -1
- package/src/tests/api/index.ts +7 -0
- package/src/tests/api/scim/groups.ts +101 -0
- package/src/tests/api/scim/shared.ts +44 -0
- package/src/tests/api/scim/users.ts +94 -0
- package/src/tests/jestEnv.ts +0 -1
- package/src/tests/jestSetup.ts +0 -2
- package/src/utilities/email.ts +2 -2
- package/tsconfig.json +3 -8
- package/src/elasticApm.ts +0 -10
- package/src/tests/logging.ts +0 -34
- package/src/utilities/appService.ts +0 -46
|
@@ -48,7 +48,7 @@ describe("/api/global/users", () => {
|
|
|
48
48
|
400
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
-
expect(res.body.message).toBe(
|
|
51
|
+
expect(res.body.message).toBe(`Unavailable`)
|
|
52
52
|
expect(sendMailMock).toHaveBeenCalledTimes(0)
|
|
53
53
|
expect(code).toBeUndefined()
|
|
54
54
|
expect(events.user.invited).toBeCalledTimes(0)
|
|
@@ -225,7 +225,9 @@ describe("/api/global/users", () => {
|
|
|
225
225
|
|
|
226
226
|
const response = await config.api.users.saveUser(user, 400)
|
|
227
227
|
|
|
228
|
-
expect(response.body.message).toBe(
|
|
228
|
+
expect(response.body.message).toBe(
|
|
229
|
+
`Email already in use: '${user.email}'`
|
|
230
|
+
)
|
|
229
231
|
expect(events.user.created).toBeCalledTimes(0)
|
|
230
232
|
})
|
|
231
233
|
|
|
@@ -237,7 +239,9 @@ describe("/api/global/users", () => {
|
|
|
237
239
|
delete user._id
|
|
238
240
|
const response = await config.api.users.saveUser(user, 400)
|
|
239
241
|
|
|
240
|
-
expect(response.body.message).toBe(
|
|
242
|
+
expect(response.body.message).toBe(
|
|
243
|
+
`Email already in use: '${user.email}'`
|
|
244
|
+
)
|
|
241
245
|
expect(events.user.created).toBeCalledTimes(0)
|
|
242
246
|
})
|
|
243
247
|
})
|
|
@@ -249,7 +253,9 @@ describe("/api/global/users", () => {
|
|
|
249
253
|
|
|
250
254
|
const response = await config.api.users.saveUser(user, 400)
|
|
251
255
|
|
|
252
|
-
expect(response.body.message).toBe(
|
|
256
|
+
expect(response.body.message).toBe(
|
|
257
|
+
`Email already in use: '${user.email}'`
|
|
258
|
+
)
|
|
253
259
|
expect(events.user.created).toBeCalledTimes(0)
|
|
254
260
|
})
|
|
255
261
|
|
package/src/api/routes/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Router from "@koa/router"
|
|
2
|
-
import { api } from "@budibase/pro"
|
|
2
|
+
import { api as pro } from "@budibase/pro"
|
|
3
3
|
import userRoutes from "./global/users"
|
|
4
4
|
import configRoutes from "./global/configs"
|
|
5
5
|
import workspaceRoutes from "./global/workspaces"
|
|
@@ -17,9 +17,6 @@ import migrationRoutes from "./system/migrations"
|
|
|
17
17
|
import accountRoutes from "./system/accounts"
|
|
18
18
|
import restoreRoutes from "./system/restore"
|
|
19
19
|
|
|
20
|
-
let userGroupRoutes = api.groups
|
|
21
|
-
let auditLogRoutes = api.auditLogs
|
|
22
|
-
|
|
23
20
|
export const routes: Router[] = [
|
|
24
21
|
configRoutes,
|
|
25
22
|
userRoutes,
|
|
@@ -33,10 +30,11 @@ export const routes: Router[] = [
|
|
|
33
30
|
statusRoutes,
|
|
34
31
|
selfRoutes,
|
|
35
32
|
licenseRoutes,
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
pro.groups,
|
|
34
|
+
pro.auditLogs,
|
|
38
35
|
migrationRoutes,
|
|
39
36
|
accountRoutes,
|
|
40
37
|
restoreRoutes,
|
|
41
38
|
eventRoutes,
|
|
39
|
+
pro.scim,
|
|
42
40
|
]
|
package/src/environment.ts
CHANGED
|
@@ -47,7 +47,6 @@ const environment = {
|
|
|
47
47
|
// flags
|
|
48
48
|
NODE_ENV: process.env.NODE_ENV,
|
|
49
49
|
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
|
50
|
-
LOG_LEVEL: process.env.LOG_LEVEL,
|
|
51
50
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
|
52
51
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
|
53
52
|
SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED,
|
package/src/index.ts
CHANGED
|
@@ -2,10 +2,6 @@ if (process.env.DD_APM_ENABLED) {
|
|
|
2
2
|
require("./ddApm")
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
if (process.env.ELASTIC_APM_ENABLED) {
|
|
6
|
-
require("./elasticApm")
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
// need to load environment first
|
|
10
6
|
import env from "./environment"
|
|
11
7
|
import { Scope } from "@sentry/node"
|
|
@@ -31,10 +27,11 @@ import api from "./api"
|
|
|
31
27
|
import * as redis from "./utilities/redis"
|
|
32
28
|
const Sentry = require("@sentry/node")
|
|
33
29
|
const koaSession = require("koa-session")
|
|
34
|
-
const logger = require("koa-pino-logger")
|
|
35
30
|
const { userAgent } = require("koa-useragent")
|
|
36
31
|
|
|
37
32
|
import destroyable from "server-destroy"
|
|
33
|
+
import { initPro } from "./initPro"
|
|
34
|
+
import { handleScimBody } from "./middleware/handleScimBody"
|
|
38
35
|
|
|
39
36
|
// configure events to use the pro audit log write
|
|
40
37
|
// can't integrate directly into backend-core due to cyclic issues
|
|
@@ -54,10 +51,12 @@ const app: Application = new Koa()
|
|
|
54
51
|
app.keys = ["secret", "key"]
|
|
55
52
|
|
|
56
53
|
// set up top level koa middleware
|
|
54
|
+
app.use(handleScimBody)
|
|
57
55
|
app.use(koaBody({ multipart: true }))
|
|
56
|
+
|
|
58
57
|
app.use(koaSession(app))
|
|
59
|
-
app.use(middleware.
|
|
60
|
-
app.use(
|
|
58
|
+
app.use(middleware.correlation)
|
|
59
|
+
app.use(middleware.pino)
|
|
61
60
|
app.use(userAgent)
|
|
62
61
|
|
|
63
62
|
// authentication
|
|
@@ -108,6 +107,7 @@ const shutdown = () => {
|
|
|
108
107
|
|
|
109
108
|
export default server.listen(parseInt(env.PORT || "4002"), async () => {
|
|
110
109
|
console.log(`Worker running on ${JSON.stringify(server.address())}`)
|
|
110
|
+
await initPro()
|
|
111
111
|
await redis.init()
|
|
112
112
|
})
|
|
113
113
|
|
package/src/initPro.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { sdk as proSdk } from "@budibase/pro"
|
|
2
|
+
import * as userSdk from "./sdk/users"
|
|
3
|
+
|
|
4
|
+
export const initPro = async () => {
|
|
5
|
+
await proSdk.init({
|
|
6
|
+
scimUserServiceConfig: {
|
|
7
|
+
functions: {
|
|
8
|
+
saveUser: userSdk.save,
|
|
9
|
+
removeUser: (id: string) => userSdk.destroy(id),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Ctx } from "@budibase/types"
|
|
2
|
+
|
|
3
|
+
export const handleScimBody = (ctx: Ctx, next: any) => {
|
|
4
|
+
var type = ctx.req.headers["content-type"] || ""
|
|
5
|
+
type = type.split(";")[0]
|
|
6
|
+
|
|
7
|
+
if (type === "application/scim+json") {
|
|
8
|
+
ctx.req.headers["content-type"] = "application/json"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return next()
|
|
12
|
+
}
|
|
@@ -18,8 +18,17 @@ export const saveMetadata = async (
|
|
|
18
18
|
if (existing) {
|
|
19
19
|
metadata._rev = existing._rev
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
try {
|
|
22
|
+
const res = await db.put(metadata)
|
|
23
|
+
metadata._rev = res.rev
|
|
24
|
+
} catch (e: any) {
|
|
25
|
+
// account can be updated frequently
|
|
26
|
+
// ignore 409
|
|
27
|
+
if (e.status !== 409) {
|
|
28
|
+
throw e
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
return metadata
|
|
24
33
|
})
|
|
25
34
|
}
|
package/src/sdk/users/users.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import env from "../../environment"
|
|
2
|
-
import * as apps from "../../utilities/appService"
|
|
3
2
|
import * as eventHelpers from "./events"
|
|
4
3
|
import {
|
|
5
4
|
accounts,
|
|
@@ -15,11 +14,12 @@ import {
|
|
|
15
14
|
utils,
|
|
16
15
|
ViewName,
|
|
17
16
|
env as coreEnv,
|
|
17
|
+
context,
|
|
18
|
+
EmailUnavailableError,
|
|
18
19
|
} from "@budibase/backend-core"
|
|
19
20
|
import {
|
|
20
21
|
AccountMetadata,
|
|
21
22
|
AllDocsResponse,
|
|
22
|
-
BulkUserResponse,
|
|
23
23
|
CloudAccount,
|
|
24
24
|
InviteUsersRequest,
|
|
25
25
|
InviteUsersResponse,
|
|
@@ -28,9 +28,10 @@ import {
|
|
|
28
28
|
PlatformUser,
|
|
29
29
|
PlatformUserByEmail,
|
|
30
30
|
RowResponse,
|
|
31
|
-
SearchUsersRequest,
|
|
32
31
|
User,
|
|
33
32
|
SaveUserOpts,
|
|
33
|
+
BulkUserCreated,
|
|
34
|
+
BulkUserDeleted,
|
|
34
35
|
Account,
|
|
35
36
|
} from "@budibase/types"
|
|
36
37
|
import { sendEmail } from "../../utilities/email"
|
|
@@ -38,8 +39,6 @@ import { EmailTemplatePurpose } from "../../constants"
|
|
|
38
39
|
import * as pro from "@budibase/pro"
|
|
39
40
|
import * as accountSdk from "../accounts"
|
|
40
41
|
|
|
41
|
-
const PAGE_LIMIT = 8
|
|
42
|
-
|
|
43
42
|
export const allUsers = async () => {
|
|
44
43
|
const db = tenancy.getGlobalDB()
|
|
45
44
|
const response = await db.allDocs(
|
|
@@ -69,43 +68,6 @@ export const getUsersByAppAccess = async (appId?: string) => {
|
|
|
69
68
|
return response
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
export const paginatedUsers = async ({
|
|
73
|
-
page,
|
|
74
|
-
email,
|
|
75
|
-
appId,
|
|
76
|
-
}: SearchUsersRequest = {}) => {
|
|
77
|
-
const db = tenancy.getGlobalDB()
|
|
78
|
-
// get one extra document, to have the next page
|
|
79
|
-
const opts: any = {
|
|
80
|
-
include_docs: true,
|
|
81
|
-
limit: PAGE_LIMIT + 1,
|
|
82
|
-
}
|
|
83
|
-
// add a startkey if the page was specified (anchor)
|
|
84
|
-
if (page) {
|
|
85
|
-
opts.startkey = page
|
|
86
|
-
}
|
|
87
|
-
// property specifies what to use for the page/anchor
|
|
88
|
-
let userList,
|
|
89
|
-
property = "_id",
|
|
90
|
-
getKey
|
|
91
|
-
if (appId) {
|
|
92
|
-
userList = await usersCore.searchGlobalUsersByApp(appId, opts)
|
|
93
|
-
getKey = (doc: any) => usersCore.getGlobalUserByAppPage(appId, doc)
|
|
94
|
-
} else if (email) {
|
|
95
|
-
userList = await usersCore.searchGlobalUsersByEmail(email, opts)
|
|
96
|
-
property = "email"
|
|
97
|
-
} else {
|
|
98
|
-
// no search, query allDocs
|
|
99
|
-
const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts))
|
|
100
|
-
userList = response.rows.map((row: any) => row.doc)
|
|
101
|
-
}
|
|
102
|
-
return dbUtils.pagination(userList, PAGE_LIMIT, {
|
|
103
|
-
paginate: true,
|
|
104
|
-
property,
|
|
105
|
-
getKey,
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
71
|
export async function getUserByEmail(email: string) {
|
|
110
72
|
return usersCore.getGlobalUserByEmail(email)
|
|
111
73
|
}
|
|
@@ -198,7 +160,7 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
|
|
|
198
160
|
if (env.MULTI_TENANCY) {
|
|
199
161
|
const tenantUser = await getPlatformUser(email)
|
|
200
162
|
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
|
201
|
-
throw
|
|
163
|
+
throw new EmailUnavailableError(email)
|
|
202
164
|
}
|
|
203
165
|
}
|
|
204
166
|
|
|
@@ -206,7 +168,7 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
|
|
|
206
168
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
207
169
|
const account = await accounts.getAccount(email)
|
|
208
170
|
if (account && account.verified && account.tenantId !== tenantId) {
|
|
209
|
-
throw
|
|
171
|
+
throw new EmailUnavailableError(email)
|
|
210
172
|
}
|
|
211
173
|
}
|
|
212
174
|
}
|
|
@@ -235,6 +197,8 @@ export async function isPreventPasswordActions(user: User, account?: Account) {
|
|
|
235
197
|
return !!(account && account.email === user.email && isSSOAccount(account))
|
|
236
198
|
}
|
|
237
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
|
|
238
202
|
export const save = async (
|
|
239
203
|
user: User,
|
|
240
204
|
opts: SaveUserOpts = {}
|
|
@@ -277,60 +241,60 @@ export const save = async (
|
|
|
277
241
|
// no id was specified - load from email instead
|
|
278
242
|
dbUser = await usersCore.getGlobalUserByEmail(email)
|
|
279
243
|
if (dbUser && dbUser._id !== _id) {
|
|
280
|
-
throw
|
|
244
|
+
throw new EmailUnavailableError(email)
|
|
281
245
|
}
|
|
282
246
|
}
|
|
283
247
|
|
|
284
|
-
|
|
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)
|
|
285
251
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
259
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
260
|
+
if (!dbUser && roles?.length) {
|
|
261
|
+
builtUser.roles = { ...roles }
|
|
262
|
+
}
|
|
297
263
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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!
|
|
303
269
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
270
|
+
if (userGroups.length > 0) {
|
|
271
|
+
for (let groupId of userGroups) {
|
|
272
|
+
groupPromises.push(pro.groups.addUsers(groupId, [_id]))
|
|
273
|
+
}
|
|
307
274
|
}
|
|
308
275
|
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
// save the user to db
|
|
313
|
-
let response = await db.put(builtUser)
|
|
314
|
-
builtUser._rev = response.rev
|
|
315
276
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
277
|
+
try {
|
|
278
|
+
// save the user to db
|
|
279
|
+
let response = await db.put(builtUser)
|
|
280
|
+
builtUser._rev = response.rev
|
|
319
281
|
|
|
320
|
-
|
|
321
|
-
|
|
282
|
+
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
|
283
|
+
await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
|
|
284
|
+
await cache.user.invalidateUser(response.id)
|
|
322
285
|
|
|
323
|
-
|
|
286
|
+
await Promise.all(groupPromises)
|
|
324
287
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
}
|
|
332
296
|
}
|
|
333
|
-
}
|
|
297
|
+
})
|
|
334
298
|
}
|
|
335
299
|
|
|
336
300
|
const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
|
|
@@ -416,7 +380,7 @@ const searchExistingEmails = async (emails: string[]) => {
|
|
|
416
380
|
export const bulkCreate = async (
|
|
417
381
|
newUsersRequested: User[],
|
|
418
382
|
groups: string[]
|
|
419
|
-
): Promise<
|
|
383
|
+
): Promise<BulkUserCreated> => {
|
|
420
384
|
const tenantId = tenancy.getTenantId()
|
|
421
385
|
|
|
422
386
|
let usersToSave: any[] = []
|
|
@@ -444,55 +408,56 @@ export const bulkCreate = async (
|
|
|
444
408
|
}
|
|
445
409
|
|
|
446
410
|
const account = await accountSdk.api.getAccountByTenantId(tenantId)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
+
)
|
|
459
425
|
)
|
|
460
|
-
)
|
|
461
|
-
})
|
|
426
|
+
})
|
|
462
427
|
|
|
463
|
-
|
|
464
|
-
|
|
428
|
+
const usersToBulkSave = await Promise.all(usersToSave)
|
|
429
|
+
await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
|
|
465
430
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
await apps.syncUserInApps(user._id)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const saved = usersToBulkSave.map(user => {
|
|
476
|
-
return {
|
|
477
|
-
_id: user._id,
|
|
478
|
-
email: user.email,
|
|
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)
|
|
479
437
|
}
|
|
480
|
-
})
|
|
481
438
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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)
|
|
488
454
|
}
|
|
489
|
-
await Promise.all(groupPromises)
|
|
490
|
-
}
|
|
491
455
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
456
|
+
return {
|
|
457
|
+
successful: saved,
|
|
458
|
+
unsuccessful,
|
|
459
|
+
}
|
|
460
|
+
})
|
|
496
461
|
}
|
|
497
462
|
|
|
498
463
|
/**
|
|
@@ -517,10 +482,10 @@ const getAccountHolderFromUserIds = async (
|
|
|
517
482
|
|
|
518
483
|
export const bulkDelete = async (
|
|
519
484
|
userIds: string[]
|
|
520
|
-
): Promise<
|
|
485
|
+
): Promise<BulkUserDeleted> => {
|
|
521
486
|
const db = tenancy.getGlobalDB()
|
|
522
487
|
|
|
523
|
-
const response:
|
|
488
|
+
const response: BulkUserDeleted = {
|
|
524
489
|
successful: [],
|
|
525
490
|
unsuccessful: [],
|
|
526
491
|
}
|
|
@@ -554,6 +519,8 @@ export const bulkDelete = async (
|
|
|
554
519
|
_deleted: true,
|
|
555
520
|
}))
|
|
556
521
|
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
|
522
|
+
|
|
523
|
+
await pro.quotas.removeUsers(toDelete.length)
|
|
557
524
|
for (let user of usersToDelete) {
|
|
558
525
|
await bulkDeleteProcessing(user)
|
|
559
526
|
}
|
|
@@ -583,7 +550,9 @@ export const bulkDelete = async (
|
|
|
583
550
|
return response
|
|
584
551
|
}
|
|
585
552
|
|
|
586
|
-
|
|
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) => {
|
|
587
556
|
const db = tenancy.getGlobalDB()
|
|
588
557
|
const dbUser = (await db.get(id)) as User
|
|
589
558
|
const userId = dbUser._id as string
|
|
@@ -593,7 +562,7 @@ export const destroy = async (id: string, currentUser: any) => {
|
|
|
593
562
|
const email = dbUser.email
|
|
594
563
|
const account = await accounts.getAccount(email)
|
|
595
564
|
if (account) {
|
|
596
|
-
if (
|
|
565
|
+
if (dbUser.userId === context.getIdentity()!._id) {
|
|
597
566
|
throw new HTTPError('Please visit "Account" to delete this user', 400)
|
|
598
567
|
} else {
|
|
599
568
|
throw new HTTPError("Account holder cannot be deleted", 400)
|
|
@@ -605,11 +574,10 @@ export const destroy = async (id: string, currentUser: any) => {
|
|
|
605
574
|
|
|
606
575
|
await db.remove(userId, dbUser._rev)
|
|
607
576
|
|
|
577
|
+
await pro.quotas.removeUsers(1)
|
|
608
578
|
await eventHelpers.handleDeleteEvents(dbUser)
|
|
609
579
|
await cache.user.invalidateUser(userId)
|
|
610
580
|
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
|
611
|
-
// let server know to sync user
|
|
612
|
-
await apps.syncUserInApps(userId, dbUser)
|
|
613
581
|
}
|
|
614
582
|
|
|
615
583
|
const bulkDeleteProcessing = async (dbUser: User) => {
|
|
@@ -618,8 +586,6 @@ const bulkDeleteProcessing = async (dbUser: User) => {
|
|
|
618
586
|
await eventHelpers.handleDeleteEvents(dbUser)
|
|
619
587
|
await cache.user.invalidateUser(userId)
|
|
620
588
|
await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })
|
|
621
|
-
// let server know to sync user
|
|
622
|
-
await apps.syncUserInApps(userId, dbUser)
|
|
623
589
|
}
|
|
624
590
|
|
|
625
591
|
export const invite = async (
|
|
@@ -20,9 +20,18 @@ import {
|
|
|
20
20
|
auth,
|
|
21
21
|
constants,
|
|
22
22
|
env as coreEnv,
|
|
23
|
+
db as dbCore,
|
|
24
|
+
encryption,
|
|
25
|
+
utils,
|
|
23
26
|
} from "@budibase/backend-core"
|
|
24
27
|
import structures, { CSRF_TOKEN } from "./structures"
|
|
25
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
SaveUserResponse,
|
|
30
|
+
User,
|
|
31
|
+
AuthToken,
|
|
32
|
+
SCIMConfig,
|
|
33
|
+
ConfigType,
|
|
34
|
+
} from "@budibase/types"
|
|
26
35
|
import API from "./api"
|
|
27
36
|
|
|
28
37
|
class TestConfiguration {
|
|
@@ -31,6 +40,7 @@ class TestConfiguration {
|
|
|
31
40
|
api: API
|
|
32
41
|
tenantId: string
|
|
33
42
|
user?: User
|
|
43
|
+
apiKey?: string
|
|
34
44
|
userPassword = "test"
|
|
35
45
|
|
|
36
46
|
constructor(opts: { openServer: boolean } = { openServer: true }) {
|
|
@@ -49,6 +59,12 @@ class TestConfiguration {
|
|
|
49
59
|
this.api = new API(this)
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
async useNewTenant() {
|
|
63
|
+
this.tenantId = structures.tenant.id()
|
|
64
|
+
|
|
65
|
+
await this.beforeAll()
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
getRequest() {
|
|
53
69
|
return this.request
|
|
54
70
|
}
|
|
@@ -201,6 +217,12 @@ class TestConfiguration {
|
|
|
201
217
|
return { [constants.Header.API_KEY]: coreEnv.INTERNAL_API_KEY }
|
|
202
218
|
}
|
|
203
219
|
|
|
220
|
+
bearerAPIHeaders() {
|
|
221
|
+
return {
|
|
222
|
+
[constants.Header.AUTHORIZATION]: `Bearer ${this.apiKey}`,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
204
226
|
adminOnlyResponse = () => {
|
|
205
227
|
return { message: "Admin user only endpoint.", status: 403 }
|
|
206
228
|
}
|
|
@@ -213,6 +235,20 @@ class TestConfiguration {
|
|
|
213
235
|
})
|
|
214
236
|
await context.doInTenant(this.tenantId!, async () => {
|
|
215
237
|
this.user = await this.createUser(user)
|
|
238
|
+
|
|
239
|
+
const db = context.getGlobalDB()
|
|
240
|
+
|
|
241
|
+
const id = dbCore.generateDevInfoID(this.user._id)
|
|
242
|
+
// TODO: dry
|
|
243
|
+
this.apiKey = encryption.encrypt(
|
|
244
|
+
`${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}`
|
|
245
|
+
)
|
|
246
|
+
const devInfo = {
|
|
247
|
+
_id: id,
|
|
248
|
+
userId: this.user._id,
|
|
249
|
+
apiKey: this.apiKey,
|
|
250
|
+
}
|
|
251
|
+
await db.put(devInfo)
|
|
216
252
|
})
|
|
217
253
|
}
|
|
218
254
|
|
|
@@ -305,6 +341,19 @@ class TestConfiguration {
|
|
|
305
341
|
controllers.config.save
|
|
306
342
|
)
|
|
307
343
|
}
|
|
344
|
+
|
|
345
|
+
// CONFIGS - SCIM
|
|
346
|
+
|
|
347
|
+
async setSCIMConfig(enabled: boolean) {
|
|
348
|
+
await this.deleteConfig(Config.SCIM)
|
|
349
|
+
const config: SCIMConfig = {
|
|
350
|
+
type: ConfigType.SCIM,
|
|
351
|
+
config: { enabled },
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await this._req(config, null, controllers.config.save)
|
|
355
|
+
return config
|
|
356
|
+
}
|
|
308
357
|
}
|
|
309
358
|
|
|
310
359
|
export default TestConfiguration
|
package/src/tests/api/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { RolesAPI } from "./roles"
|
|
|
15
15
|
import { TemplatesAPI } from "./templates"
|
|
16
16
|
import { LicenseAPI } from "./license"
|
|
17
17
|
import { AuditLogAPI } from "./auditLogs"
|
|
18
|
+
import { ScimUsersAPI } from "./scim/users"
|
|
19
|
+
import { ScimGroupsAPI } from "./scim/groups"
|
|
20
|
+
|
|
18
21
|
export default class API {
|
|
19
22
|
accounts: AccountAPI
|
|
20
23
|
auth: AuthAPI
|
|
@@ -32,6 +35,8 @@ export default class API {
|
|
|
32
35
|
templates: TemplatesAPI
|
|
33
36
|
license: LicenseAPI
|
|
34
37
|
auditLogs: AuditLogAPI
|
|
38
|
+
scimUsersAPI: ScimUsersAPI
|
|
39
|
+
scimGroupsAPI: ScimGroupsAPI
|
|
35
40
|
|
|
36
41
|
constructor(config: TestConfiguration) {
|
|
37
42
|
this.accounts = new AccountAPI(config)
|
|
@@ -50,5 +55,7 @@ export default class API {
|
|
|
50
55
|
this.templates = new TemplatesAPI(config)
|
|
51
56
|
this.license = new LicenseAPI(config)
|
|
52
57
|
this.auditLogs = new AuditLogAPI(config)
|
|
58
|
+
this.scimUsersAPI = new ScimUsersAPI(config)
|
|
59
|
+
this.scimGroupsAPI = new ScimGroupsAPI(config)
|
|
53
60
|
}
|
|
54
61
|
}
|