@budibase/worker 3.35.3 → 3.36.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/__mocks__/@budibase/pro.ts +4 -0
- package/package.json +4 -4
- package/src/api/controllers/global/auth.ts +13 -1
- package/src/api/controllers/global/configs.ts +35 -0
- package/src/api/controllers/global/scim/users.ts +1 -1
- package/src/api/routes/global/license.ts +10 -4
- package/src/api/routes/global/tests/configs.spec.ts +66 -0
- package/src/api/routes/global/tests/license.spec.ts +132 -0
- package/src/api/routes/global/tests/scim.spec.ts +54 -3
- package/src/api/routes/global/tests/users.spec.ts +8 -0
- package/src/tests/api/license.ts +7 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.36.1",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prebuild": "rimraf dist/",
|
|
16
16
|
"build": "node ./scripts/build.js",
|
|
17
17
|
"postbuild": "copyfiles -f ../../yarn.lock ./dist/",
|
|
18
|
-
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
|
18
|
+
"check:types": "tsc -p tsconfig.json --noEmit --incremental --paths null",
|
|
19
19
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
|
20
20
|
"run:docker": "node --enable-source-maps dist/index.js",
|
|
21
21
|
"debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"lodash": "4.18.1",
|
|
56
56
|
"marked": "^15.0.11",
|
|
57
57
|
"node-fetch": "2.6.7",
|
|
58
|
-
"nodemailer": "8.0.
|
|
58
|
+
"nodemailer": "8.0.5",
|
|
59
59
|
"pouchdb": "9.0.0",
|
|
60
60
|
"scim-patch": "^0.8.1",
|
|
61
61
|
"scim2-parse-filter": "^0.2.8",
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"supertest": "6.3.3",
|
|
80
80
|
"timekeeper": "2.2.0"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "7a83e0e9dc810a0a6e41b006b8a6f26c749c75c0"
|
|
83
83
|
}
|
|
@@ -95,7 +95,10 @@ async function passportCallback(
|
|
|
95
95
|
const loginResult = await authSdk.loginUser(user)
|
|
96
96
|
|
|
97
97
|
// set a cookie for browser access
|
|
98
|
-
setCookie(ctx, loginResult.token, Cookie.Auth, {
|
|
98
|
+
setCookie(ctx, loginResult.token, Cookie.Auth, {
|
|
99
|
+
sign: false,
|
|
100
|
+
httpOnly: true,
|
|
101
|
+
})
|
|
99
102
|
// set the token in a header as well for APIs
|
|
100
103
|
ctx.set(Header.TOKEN, loginResult.token)
|
|
101
104
|
|
|
@@ -282,14 +285,20 @@ export const datasourcePreAuth = async (
|
|
|
282
285
|
next: Next
|
|
283
286
|
) => {
|
|
284
287
|
const provider = ctx.params.provider
|
|
288
|
+
const returnPath =
|
|
289
|
+
typeof ctx.query.returnPath === "string" ? ctx.query.returnPath : undefined
|
|
285
290
|
const { middleware } = require(`@budibase/backend-core`)
|
|
286
291
|
const handler = middleware.datasource[provider]
|
|
292
|
+
if (!handler) {
|
|
293
|
+
ctx.throw(400, "Unsupported datasource provider")
|
|
294
|
+
}
|
|
287
295
|
|
|
288
296
|
setCookie(
|
|
289
297
|
ctx,
|
|
290
298
|
{
|
|
291
299
|
provider,
|
|
292
300
|
appId: ctx.query.appId,
|
|
301
|
+
returnPath,
|
|
293
302
|
},
|
|
294
303
|
Cookie.DatasourceAuth
|
|
295
304
|
)
|
|
@@ -308,6 +317,9 @@ export const datasourceAuth = async (ctx: UserCtx<void, void>, next: Next) => {
|
|
|
308
317
|
const provider = authStateCookie.provider
|
|
309
318
|
const { middleware } = require(`@budibase/backend-core`)
|
|
310
319
|
const handler = middleware.datasource[provider]
|
|
320
|
+
if (!handler) {
|
|
321
|
+
ctx.throw(400, "Unsupported datasource provider")
|
|
322
|
+
}
|
|
311
323
|
return handler.postAuth(passport, ctx, next)
|
|
312
324
|
}
|
|
313
325
|
|
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
OIDCLogosConfig,
|
|
37
37
|
PASSWORD_REPLACEMENT,
|
|
38
38
|
RecaptchaInnerConfig,
|
|
39
|
+
SCIMDisableAction,
|
|
40
|
+
SCIMInnerConfig,
|
|
39
41
|
SaveConfigRequest,
|
|
40
42
|
SaveConfigResponse,
|
|
41
43
|
SettingsBrandingConfig,
|
|
@@ -346,12 +348,26 @@ async function processTranslationsConfig(
|
|
|
346
348
|
ctx.request.body.config = prepareTranslationsConfig(ctx, config)
|
|
347
349
|
}
|
|
348
350
|
|
|
351
|
+
async function processSCIMConfig(
|
|
352
|
+
newConfig: SCIMInnerConfig,
|
|
353
|
+
existingConfig: SCIMInnerConfig | undefined
|
|
354
|
+
): Promise<SCIMDisableAction | undefined> {
|
|
355
|
+
const { disableAction } = newConfig
|
|
356
|
+
delete newConfig.disableAction
|
|
357
|
+
|
|
358
|
+
const isBeingDisabled = existingConfig?.enabled && !newConfig.enabled
|
|
359
|
+
if (isBeingDisabled && disableAction) {
|
|
360
|
+
return disableAction
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
349
364
|
export async function save(
|
|
350
365
|
ctx: UserCtx<SaveConfigRequest, SaveConfigResponse>
|
|
351
366
|
) {
|
|
352
367
|
const body = ctx.request.body
|
|
353
368
|
const type = body.type
|
|
354
369
|
const config = body.config
|
|
370
|
+
let scimDisableAction: SCIMDisableAction | undefined
|
|
355
371
|
|
|
356
372
|
const existingConfig = await configs.getConfig(type)
|
|
357
373
|
let eventFns = await getEventFns(ctx.request.body, existingConfig)
|
|
@@ -380,6 +396,12 @@ export async function save(
|
|
|
380
396
|
case ConfigType.TRANSLATIONS:
|
|
381
397
|
await processTranslationsConfig(ctx, config)
|
|
382
398
|
break
|
|
399
|
+
case ConfigType.SCIM:
|
|
400
|
+
scimDisableAction = await processSCIMConfig(
|
|
401
|
+
config,
|
|
402
|
+
existingConfig?.config
|
|
403
|
+
)
|
|
404
|
+
break
|
|
383
405
|
}
|
|
384
406
|
} catch (err: any) {
|
|
385
407
|
ctx.throw(400, err?.message || err)
|
|
@@ -425,6 +447,19 @@ export async function save(
|
|
|
425
447
|
await fn()
|
|
426
448
|
}
|
|
427
449
|
|
|
450
|
+
if (scimDisableAction) {
|
|
451
|
+
const tenantId = tenancy.getTenantId()
|
|
452
|
+
setImmediate(async () => {
|
|
453
|
+
try {
|
|
454
|
+
await tenancy.doInTenant(tenantId, () =>
|
|
455
|
+
pro.scimUsers.handleDisable(scimDisableAction!)
|
|
456
|
+
)
|
|
457
|
+
} catch (e) {
|
|
458
|
+
console.error("Error processing SCIM users on disable:", e)
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
428
463
|
ctx.body = {
|
|
429
464
|
type,
|
|
430
465
|
_id: response.id,
|
|
@@ -107,7 +107,7 @@ export const update = async (ctx: Ctx<ScimUpdateRequest, ScimUserResponse>) => {
|
|
|
107
107
|
ctx.throw(500)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
const userToUpdate = mappers.user.fromScimUser(patchedScimUser)
|
|
110
|
+
const userToUpdate = mappers.user.fromScimUser(patchedScimUser, user.roles)
|
|
111
111
|
await scimUsers.update(userToUpdate, { allowChangingEmail: true })
|
|
112
112
|
|
|
113
113
|
ctx.body = mappers.user.toScimUserResponse(userToUpdate)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as controller from "../../controllers/global/license"
|
|
2
2
|
import { middleware } from "@budibase/backend-core"
|
|
3
3
|
import Joi from "joi"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
adminRoutes,
|
|
6
|
+
builderOrAdminRoutes,
|
|
7
|
+
loggedInRoutes,
|
|
8
|
+
} from "../endpointGroups"
|
|
5
9
|
|
|
6
10
|
const activateLicenseKeyValidator = middleware.joiValidator.body(
|
|
7
11
|
Joi.object({
|
|
@@ -15,9 +19,12 @@ const activateOfflineLicenseValidator = middleware.joiValidator.body(
|
|
|
15
19
|
}).required()
|
|
16
20
|
)
|
|
17
21
|
|
|
18
|
-
loggedInRoutes
|
|
22
|
+
loggedInRoutes.get("/api/global/license/usage", controller.getQuotaUsage)
|
|
23
|
+
|
|
24
|
+
builderOrAdminRoutes.get("/api/global/license/key", controller.getLicenseKey)
|
|
25
|
+
|
|
26
|
+
adminRoutes
|
|
19
27
|
.post("/api/global/license/refresh", controller.refresh)
|
|
20
|
-
.get("/api/global/license/usage", controller.getQuotaUsage)
|
|
21
28
|
.get("/api/global/install", controller.getInstallInfo)
|
|
22
29
|
// LICENSE KEY
|
|
23
30
|
.post(
|
|
@@ -25,7 +32,6 @@ loggedInRoutes
|
|
|
25
32
|
activateLicenseKeyValidator,
|
|
26
33
|
controller.activateLicenseKey
|
|
27
34
|
)
|
|
28
|
-
.get("/api/global/license/key", controller.getLicenseKey)
|
|
29
35
|
.delete("/api/global/license/key", controller.deleteLicenseKey)
|
|
30
36
|
// OFFLINE LICENSE
|
|
31
37
|
.post(
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ConfigType,
|
|
6
6
|
GetPublicSettingsResponse,
|
|
7
7
|
PKCEMethod,
|
|
8
|
+
SCIMConfig,
|
|
8
9
|
TranslationsConfig,
|
|
9
10
|
} from "@budibase/types"
|
|
10
11
|
import { TestConfiguration, mocks, structures } from "../../../../tests"
|
|
@@ -402,6 +403,71 @@ describe("configs", () => {
|
|
|
402
403
|
})
|
|
403
404
|
})
|
|
404
405
|
|
|
406
|
+
describe("scim", () => {
|
|
407
|
+
const scimConfig = (enabled: boolean, disableAction?: string): SCIMConfig =>
|
|
408
|
+
({
|
|
409
|
+
type: ConfigType.SCIM,
|
|
410
|
+
config: { enabled, ...(disableAction ? { disableAction } : {}) },
|
|
411
|
+
}) as SCIMConfig
|
|
412
|
+
|
|
413
|
+
beforeEach(async () => {
|
|
414
|
+
await config.deleteConfig(ConfigType.SCIM)
|
|
415
|
+
jest.clearAllMocks()
|
|
416
|
+
mocks.pro.scimUsers.handleDisable.mockResolvedValue(undefined)
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
afterEach(async () => {
|
|
420
|
+
await config.deleteConfig(ConfigType.SCIM)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it("calls handleDisable with 'remove' when SCIM is disabled with remove action", async () => {
|
|
424
|
+
await config.api.configs.saveConfig(scimConfig(true))
|
|
425
|
+
jest.clearAllMocks()
|
|
426
|
+
|
|
427
|
+
await config.api.configs.saveConfig(scimConfig(false, "remove"))
|
|
428
|
+
await new Promise<void>(resolve => setImmediate(resolve))
|
|
429
|
+
|
|
430
|
+
expect(mocks.pro.scimUsers.handleDisable).toHaveBeenCalledTimes(1)
|
|
431
|
+
expect(mocks.pro.scimUsers.handleDisable).toHaveBeenCalledWith("remove")
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it("calls handleDisable with 'convert' when SCIM is disabled with convert action", async () => {
|
|
435
|
+
await config.api.configs.saveConfig(scimConfig(true))
|
|
436
|
+
jest.clearAllMocks()
|
|
437
|
+
|
|
438
|
+
await config.api.configs.saveConfig(scimConfig(false, "convert"))
|
|
439
|
+
await new Promise<void>(resolve => setImmediate(resolve))
|
|
440
|
+
|
|
441
|
+
expect(mocks.pro.scimUsers.handleDisable).toHaveBeenCalledTimes(1)
|
|
442
|
+
expect(mocks.pro.scimUsers.handleDisable).toHaveBeenCalledWith("convert")
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it("does not call handleDisable when SCIM is disabled without a disableAction", async () => {
|
|
446
|
+
await config.api.configs.saveConfig(scimConfig(true))
|
|
447
|
+
jest.clearAllMocks()
|
|
448
|
+
|
|
449
|
+
await config.api.configs.saveConfig(scimConfig(false))
|
|
450
|
+
await new Promise<void>(resolve => setImmediate(resolve))
|
|
451
|
+
|
|
452
|
+
expect(mocks.pro.scimUsers.handleDisable).not.toHaveBeenCalled()
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it("does not call handleDisable when SCIM is being enabled", async () => {
|
|
456
|
+
await config.api.configs.saveConfig(scimConfig(true, "remove"))
|
|
457
|
+
await new Promise<void>(resolve => setImmediate(resolve))
|
|
458
|
+
|
|
459
|
+
expect(mocks.pro.scimUsers.handleDisable).not.toHaveBeenCalled()
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it("does not persist disableAction to the saved config", async () => {
|
|
463
|
+
await config.api.configs.saveConfig(scimConfig(true))
|
|
464
|
+
await config.api.configs.saveConfig(scimConfig(false, "remove"))
|
|
465
|
+
|
|
466
|
+
const saved = await config.api.configs.getConfig(ConfigType.SCIM)
|
|
467
|
+
expect(saved.config).not.toHaveProperty("disableAction")
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
|
|
405
471
|
describe("GET /api/global/configs/checklist", () => {
|
|
406
472
|
it("should return the correct checklist", async () => {
|
|
407
473
|
await config.saveSmtpConfig()
|
|
@@ -6,6 +6,26 @@ const quotas = mocks.pro.quotas
|
|
|
6
6
|
describe("/api/global/license", () => {
|
|
7
7
|
const config = new TestConfiguration()
|
|
8
8
|
|
|
9
|
+
async function createNonAdminUser() {
|
|
10
|
+
const user = await config.createUser()
|
|
11
|
+
await config.login(user)
|
|
12
|
+
return user
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function createBuilderUser() {
|
|
16
|
+
const user = await config.createUser(structures.users.builderUser())
|
|
17
|
+
await config.login(user)
|
|
18
|
+
return user
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function createCreatorUser() {
|
|
22
|
+
const user = await config.createUser(
|
|
23
|
+
structures.users.user({ builder: { creator: true } })
|
|
24
|
+
)
|
|
25
|
+
await config.login(user)
|
|
26
|
+
return user
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
beforeAll(async () => {
|
|
10
30
|
await config.beforeAll()
|
|
11
31
|
})
|
|
@@ -14,6 +34,10 @@ describe("/api/global/license", () => {
|
|
|
14
34
|
await config.afterAll()
|
|
15
35
|
})
|
|
16
36
|
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
mocks.licenses.useUnlimited()
|
|
39
|
+
})
|
|
40
|
+
|
|
17
41
|
afterEach(() => {
|
|
18
42
|
jest.resetAllMocks()
|
|
19
43
|
})
|
|
@@ -34,6 +58,19 @@ describe("/api/global/license", () => {
|
|
|
34
58
|
expect(res.status).toBe(200)
|
|
35
59
|
expect(res.body).toEqual(usage)
|
|
36
60
|
})
|
|
61
|
+
|
|
62
|
+
it("allows non-admin access", async () => {
|
|
63
|
+
const user = await createNonAdminUser()
|
|
64
|
+
const usage = structures.quotas.usage()
|
|
65
|
+
quotas.getQuotaUsage.mockResolvedValue(usage)
|
|
66
|
+
|
|
67
|
+
const res = await config.withUser(user, () =>
|
|
68
|
+
config.api.license.getUsage()
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(res.status).toBe(200)
|
|
72
|
+
expect(res.body).toEqual(usage)
|
|
73
|
+
})
|
|
37
74
|
})
|
|
38
75
|
|
|
39
76
|
describe("POST /api/global/license/key", () => {
|
|
@@ -53,6 +90,35 @@ describe("/api/global/license", () => {
|
|
|
53
90
|
const res = await config.api.license.getLicenseKey()
|
|
54
91
|
expect(res.status).toBe(404)
|
|
55
92
|
})
|
|
93
|
+
|
|
94
|
+
it("allows builder access", async () => {
|
|
95
|
+
const user = await createBuilderUser()
|
|
96
|
+
licensing.keys.getLicenseKey.mockResolvedValue("licenseKey")
|
|
97
|
+
|
|
98
|
+
const res = await config.withUser(user, () =>
|
|
99
|
+
config.api.license.getLicenseKey()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expect(res.status).toBe(200)
|
|
103
|
+
expect(res.body).toEqual({
|
|
104
|
+
licenseKey: "*",
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("allows creator access", async () => {
|
|
109
|
+
const user = await createCreatorUser()
|
|
110
|
+
licensing.keys.getLicenseKey.mockResolvedValue("licenseKey")
|
|
111
|
+
|
|
112
|
+
const res = await config.withUser(user, () =>
|
|
113
|
+
config.api.license.getLicenseKey()
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
expect(res.status).toBe(200)
|
|
117
|
+
expect(res.body).toEqual({
|
|
118
|
+
licenseKey: "*",
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
56
122
|
it("returns 200 + license key", async () => {
|
|
57
123
|
licensing.keys.getLicenseKey.mockResolvedValue("licenseKey")
|
|
58
124
|
const res = await config.api.license.getLicenseKey()
|
|
@@ -120,4 +186,70 @@ describe("/api/global/license", () => {
|
|
|
120
186
|
})
|
|
121
187
|
})
|
|
122
188
|
})
|
|
189
|
+
|
|
190
|
+
describe("authorisation", () => {
|
|
191
|
+
it.each([
|
|
192
|
+
[
|
|
193
|
+
"GET /api/global/license/key",
|
|
194
|
+
() => config.api.license.getLicenseKey(),
|
|
195
|
+
{ message: "Admin/Builder user only endpoint.", status: 403 },
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
"POST /api/global/license/refresh",
|
|
199
|
+
() => config.api.license.refresh(),
|
|
200
|
+
config.adminOnlyResponse(),
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"GET /api/global/install",
|
|
204
|
+
() => config.api.license.getInstallInfo(),
|
|
205
|
+
config.adminOnlyResponse(),
|
|
206
|
+
],
|
|
207
|
+
[
|
|
208
|
+
"POST /api/global/license/key",
|
|
209
|
+
() =>
|
|
210
|
+
config.api.license.activateLicenseKey({
|
|
211
|
+
licenseKey: "licenseKey",
|
|
212
|
+
}),
|
|
213
|
+
config.adminOnlyResponse(),
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
"DELETE /api/global/license/key",
|
|
217
|
+
() => config.api.license.deleteLicenseKey(),
|
|
218
|
+
config.adminOnlyResponse(),
|
|
219
|
+
],
|
|
220
|
+
[
|
|
221
|
+
"POST /api/global/license/offline",
|
|
222
|
+
() =>
|
|
223
|
+
config.api.license.activateOfflineLicense({
|
|
224
|
+
offlineLicenseToken: "offlineLicenseToken",
|
|
225
|
+
}),
|
|
226
|
+
config.adminOnlyResponse(),
|
|
227
|
+
],
|
|
228
|
+
[
|
|
229
|
+
"GET /api/global/license/offline",
|
|
230
|
+
() => config.api.license.getOfflineLicense(),
|
|
231
|
+
config.adminOnlyResponse(),
|
|
232
|
+
],
|
|
233
|
+
[
|
|
234
|
+
"DELETE /api/global/license/offline",
|
|
235
|
+
() => config.api.license.deleteOfflineLicense(),
|
|
236
|
+
config.adminOnlyResponse(),
|
|
237
|
+
],
|
|
238
|
+
[
|
|
239
|
+
"GET /api/global/license/offline/identifier",
|
|
240
|
+
() => config.api.license.getOfflineLicenseIdentifier(),
|
|
241
|
+
config.adminOnlyResponse(),
|
|
242
|
+
],
|
|
243
|
+
])(
|
|
244
|
+
"returns 403 for non-admin access to %s",
|
|
245
|
+
async (_path, request, expectedBody) => {
|
|
246
|
+
const user = await createNonAdminUser()
|
|
247
|
+
|
|
248
|
+
const res = await config.withUser(user, () => request())
|
|
249
|
+
|
|
250
|
+
expect(res.status).toBe(403)
|
|
251
|
+
expect(res.body).toEqual(expectedBody)
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
})
|
|
123
255
|
})
|
|
@@ -364,9 +364,12 @@ describe("scim", () => {
|
|
|
364
364
|
})
|
|
365
365
|
|
|
366
366
|
it("creating an external user that conflicts an internal one syncs the existing user", async () => {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
const workspaceId = "app_scim_sync_roles"
|
|
368
|
+
const explicitRoles = { [workspaceId]: "BASIC" }
|
|
369
|
+
const { body: internalUser } = await config.api.users.saveUser({
|
|
370
|
+
...structures.users.user(),
|
|
371
|
+
roles: explicitRoles,
|
|
372
|
+
})
|
|
370
373
|
|
|
371
374
|
const scimUserData = {
|
|
372
375
|
externalId: structures.uuid(),
|
|
@@ -410,6 +413,14 @@ describe("scim", () => {
|
|
|
410
413
|
}
|
|
411
414
|
|
|
412
415
|
expect(res).toEqual(expectedScimUser)
|
|
416
|
+
|
|
417
|
+
expect(
|
|
418
|
+
(await config.api.users.getUser(internalUser._id!)).body
|
|
419
|
+
).toEqual(
|
|
420
|
+
expect.objectContaining({
|
|
421
|
+
roles: explicitRoles,
|
|
422
|
+
})
|
|
423
|
+
)
|
|
413
424
|
})
|
|
414
425
|
|
|
415
426
|
it("a user cannot be SCIM synchronised with another SCIM user", async () => {
|
|
@@ -516,6 +527,46 @@ describe("scim", () => {
|
|
|
516
527
|
expect(persistedUser).toEqual(expectedScimUser)
|
|
517
528
|
})
|
|
518
529
|
|
|
530
|
+
it("preserves explicit roles when updating an existing SCIM user", async () => {
|
|
531
|
+
const workspaceId = "app_scim_patch_roles"
|
|
532
|
+
const explicitRoles = { [workspaceId]: "BASIC" }
|
|
533
|
+
const { body: internalUser } = await config.api.users.saveUser({
|
|
534
|
+
...structures.users.user(),
|
|
535
|
+
roles: explicitRoles,
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
const syncedUser = await config.api.scimUsersAPI.post(
|
|
539
|
+
{
|
|
540
|
+
body: structures.scim.createUserRequest({
|
|
541
|
+
email: internalUser.email,
|
|
542
|
+
}),
|
|
543
|
+
},
|
|
544
|
+
{ expect: 200 }
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
const newFamilyName = structures.generator.last()
|
|
548
|
+
const body: ScimUpdateRequest = {
|
|
549
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
550
|
+
Operations: [
|
|
551
|
+
{
|
|
552
|
+
op: "Replace",
|
|
553
|
+
path: "name.familyName",
|
|
554
|
+
value: newFamilyName,
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
await patchScimUser({ id: syncedUser.id, body })
|
|
560
|
+
|
|
561
|
+
expect((await config.api.users.getUser(syncedUser.id)).body).toEqual(
|
|
562
|
+
expect.objectContaining({
|
|
563
|
+
_id: syncedUser.id,
|
|
564
|
+
lastName: newFamilyName,
|
|
565
|
+
roles: explicitRoles,
|
|
566
|
+
})
|
|
567
|
+
)
|
|
568
|
+
})
|
|
569
|
+
|
|
519
570
|
it.each([false, "false", "False"])(
|
|
520
571
|
"deactivating an active user (sending %s) will delete it",
|
|
521
572
|
async activeValue => {
|
|
@@ -1138,6 +1138,14 @@ describe("/api/global/users", () => {
|
|
|
1138
1138
|
await config.api.users.searchUsers({}, { status: 403, noHeaders: true })
|
|
1139
1139
|
})
|
|
1140
1140
|
|
|
1141
|
+
it("should throw an error if a public route is injected in the query string", async () => {
|
|
1142
|
+
await config.request
|
|
1143
|
+
.post("/api/global/users/search?x=/api/system/status")
|
|
1144
|
+
.send({})
|
|
1145
|
+
.expect("Content-Type", /json/)
|
|
1146
|
+
.expect(403)
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1141
1149
|
it("should be able to search using logical conditions", async () => {
|
|
1142
1150
|
const user = await config.createUser()
|
|
1143
1151
|
const response = await config.api.users.searchUsers({
|
package/src/tests/api/license.ts
CHANGED
|
@@ -10,12 +10,17 @@ export class LicenseAPI extends TestAPI {
|
|
|
10
10
|
.post("/api/global/license/refresh")
|
|
11
11
|
.set(this.config.defaultHeaders())
|
|
12
12
|
}
|
|
13
|
-
getUsage = async () => {
|
|
13
|
+
getUsage = async (status = 200) => {
|
|
14
14
|
return this.request
|
|
15
15
|
.get("/api/global/license/usage")
|
|
16
16
|
.set(this.config.defaultHeaders())
|
|
17
17
|
.expect("Content-Type", /json/)
|
|
18
|
-
.expect(
|
|
18
|
+
.expect(status)
|
|
19
|
+
}
|
|
20
|
+
getInstallInfo = async () => {
|
|
21
|
+
return this.request
|
|
22
|
+
.get("/api/global/install")
|
|
23
|
+
.set(this.config.defaultHeaders())
|
|
19
24
|
}
|
|
20
25
|
activateLicenseKey = async (body: ActivateLicenseKeyRequest) => {
|
|
21
26
|
return this.request
|