@budibase/worker 2.4.44-alpha.2 → 2.4.44-alpha.4
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 +8 -6
- package/src/api/controllers/global/users.ts +2 -2
- package/src/api/routes/global/configs.ts +9 -1
- package/src/api/routes/global/tests/scim.spec.ts +892 -0
- package/src/api/routes/index.ts +4 -6
- package/src/index.ts +5 -0
- package/src/initPro.ts +13 -0
- package/src/middleware/handleScimBody.ts +12 -0
- package/src/sdk/users/users.ts +3 -41
- package/src/tests/TestConfiguration.ts +50 -1
- package/src/tests/api/index.ts +7 -0
- package/src/tests/api/scim/groups.ts +94 -0
- package/src/tests/api/scim/shared.ts +43 -0
- package/src/tests/api/scim/users.ts +94 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
import tk from "timekeeper"
|
|
2
|
+
import _ from "lodash"
|
|
3
|
+
import { mocks, structures } from "@budibase/backend-core/tests"
|
|
4
|
+
import {
|
|
5
|
+
ScimGroupResponse,
|
|
6
|
+
ScimUpdateRequest,
|
|
7
|
+
ScimUserResponse,
|
|
8
|
+
} from "@budibase/types"
|
|
9
|
+
import { TestConfiguration } from "../../../../tests"
|
|
10
|
+
import { events } from "@budibase/backend-core"
|
|
11
|
+
|
|
12
|
+
mocks.licenses.useScimIntegration()
|
|
13
|
+
|
|
14
|
+
describe("scim", () => {
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
jest.resetAllMocks()
|
|
17
|
+
tk.freeze(mocks.date.MOCK_DATE)
|
|
18
|
+
mocks.licenses.useScimIntegration()
|
|
19
|
+
|
|
20
|
+
await config.setSCIMConfig(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const config = new TestConfiguration()
|
|
24
|
+
|
|
25
|
+
const unauthorisedTests = (fn: (...params: any) => Promise<any>) => {
|
|
26
|
+
describe("unauthorised calls", () => {
|
|
27
|
+
it("unauthorised calls are not allowed", async () => {
|
|
28
|
+
const response = await fn(...Array(fn.length - 1).fill({}), {
|
|
29
|
+
setHeaders: false,
|
|
30
|
+
expect: 403,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(response).toEqual({ message: "Tenant id not set", status: 403 })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("cannot be called when feature is disabled", async () => {
|
|
37
|
+
mocks.licenses.useCloudFree()
|
|
38
|
+
const response = await fn(...Array(fn.length - 1).fill({}), {
|
|
39
|
+
expect: 400,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(response).toEqual({
|
|
43
|
+
error: {
|
|
44
|
+
code: "feature_disabled",
|
|
45
|
+
featureName: "scim",
|
|
46
|
+
},
|
|
47
|
+
message: "scim is not currently enabled",
|
|
48
|
+
status: 400,
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("cannot be called when feature is enabled but the config disabled", async () => {
|
|
53
|
+
await config.setSCIMConfig(false)
|
|
54
|
+
const response = await fn(...Array(fn.length - 1).fill({}), {
|
|
55
|
+
expect: 400,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(response).toEqual({
|
|
59
|
+
error: {
|
|
60
|
+
code: "feature_disabled",
|
|
61
|
+
featureName: "scim",
|
|
62
|
+
},
|
|
63
|
+
message: "scim is not currently enabled",
|
|
64
|
+
status: 400,
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeAll(async () => {
|
|
71
|
+
await config.beforeAll()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
afterAll(async () => {
|
|
75
|
+
await config.afterAll()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe("/api/global/scim/v2/users", () => {
|
|
79
|
+
describe("GET /api/global/scim/v2/users", () => {
|
|
80
|
+
const getScimUsers = config.api.scimUsersAPI.get
|
|
81
|
+
|
|
82
|
+
unauthorisedTests(getScimUsers)
|
|
83
|
+
|
|
84
|
+
describe("no users exist", () => {
|
|
85
|
+
it("should retrieve empty list", async () => {
|
|
86
|
+
const response = await getScimUsers()
|
|
87
|
+
|
|
88
|
+
expect(response).toEqual({
|
|
89
|
+
Resources: [],
|
|
90
|
+
itemsPerPage: 20,
|
|
91
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
92
|
+
startIndex: 1,
|
|
93
|
+
totalResults: 0,
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe("multiple users exist", () => {
|
|
99
|
+
const userCount = 30
|
|
100
|
+
let users: ScimUserResponse[]
|
|
101
|
+
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
users = []
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < userCount; i++) {
|
|
106
|
+
const body = structures.scim.createUserRequest()
|
|
107
|
+
users.push(await config.api.scimUsersAPI.post({ body }))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
users = users.sort((a, b) => (a.id > b.id ? 1 : -1))
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("fetches full first page", async () => {
|
|
114
|
+
const response = await getScimUsers()
|
|
115
|
+
|
|
116
|
+
expect(response).toEqual({
|
|
117
|
+
Resources: expect.arrayContaining(users.slice(0, 20)),
|
|
118
|
+
itemsPerPage: 20,
|
|
119
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
120
|
+
startIndex: 1,
|
|
121
|
+
totalResults: userCount,
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("fetches second page", async () => {
|
|
126
|
+
const response = await getScimUsers({ params: { startIndex: 20 } })
|
|
127
|
+
|
|
128
|
+
expect(response).toEqual({
|
|
129
|
+
Resources: users.slice(20),
|
|
130
|
+
itemsPerPage: 20,
|
|
131
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
132
|
+
startIndex: 21,
|
|
133
|
+
totalResults: userCount,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("can filter by user name", async () => {
|
|
138
|
+
const userToFetch = _.sample(users)
|
|
139
|
+
|
|
140
|
+
const response = await getScimUsers({
|
|
141
|
+
params: {
|
|
142
|
+
filter: encodeURI(`userName eq "${userToFetch?.userName}"`),
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(response).toEqual({
|
|
147
|
+
Resources: [userToFetch],
|
|
148
|
+
itemsPerPage: 20,
|
|
149
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
150
|
+
startIndex: 1,
|
|
151
|
+
totalResults: 1,
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("can filter by external id", async () => {
|
|
156
|
+
const userToFetch = _.sample(users)
|
|
157
|
+
|
|
158
|
+
const response = await getScimUsers({
|
|
159
|
+
params: {
|
|
160
|
+
filter: encodeURI(`externalId eq "${userToFetch?.externalId}"`),
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
expect(response).toEqual({
|
|
165
|
+
Resources: [userToFetch],
|
|
166
|
+
itemsPerPage: 20,
|
|
167
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
168
|
+
startIndex: 1,
|
|
169
|
+
totalResults: 1,
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("can filter by email", async () => {
|
|
174
|
+
const userToFetch = _.sample(users)
|
|
175
|
+
|
|
176
|
+
const response = await getScimUsers({
|
|
177
|
+
params: {
|
|
178
|
+
filter: encodeURI(
|
|
179
|
+
`emails[type eq "work"].value eq "${userToFetch?.emails[0].value}"`
|
|
180
|
+
),
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
expect(response).toEqual({
|
|
185
|
+
Resources: [userToFetch],
|
|
186
|
+
itemsPerPage: 20,
|
|
187
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
188
|
+
startIndex: 1,
|
|
189
|
+
totalResults: 1,
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe("POST /api/global/scim/v2/users", () => {
|
|
196
|
+
const postScimUser = config.api.scimUsersAPI.post
|
|
197
|
+
|
|
198
|
+
beforeAll(async () => {
|
|
199
|
+
await config.useNewTenant()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
unauthorisedTests(postScimUser)
|
|
203
|
+
|
|
204
|
+
describe("no users exist", () => {
|
|
205
|
+
it("a new user can be created and persisted", async () => {
|
|
206
|
+
const mockedTime = new Date(structures.generator.timestamp())
|
|
207
|
+
tk.freeze(mockedTime)
|
|
208
|
+
|
|
209
|
+
const userData = {
|
|
210
|
+
externalId: structures.uuid(),
|
|
211
|
+
email: structures.generator.email(),
|
|
212
|
+
firstName: structures.generator.first(),
|
|
213
|
+
lastName: structures.generator.last(),
|
|
214
|
+
username: structures.generator.name(),
|
|
215
|
+
}
|
|
216
|
+
const body = structures.scim.createUserRequest(userData)
|
|
217
|
+
|
|
218
|
+
const response = await postScimUser({ body })
|
|
219
|
+
|
|
220
|
+
const expectedScimUser = {
|
|
221
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
|
222
|
+
id: expect.any(String),
|
|
223
|
+
externalId: userData.externalId,
|
|
224
|
+
meta: {
|
|
225
|
+
resourceType: "User",
|
|
226
|
+
created: mockedTime.toISOString(),
|
|
227
|
+
lastModified: mockedTime.toISOString(),
|
|
228
|
+
},
|
|
229
|
+
userName: userData.username,
|
|
230
|
+
name: {
|
|
231
|
+
formatted: `${userData.firstName} ${userData.lastName}`,
|
|
232
|
+
familyName: userData.lastName,
|
|
233
|
+
givenName: userData.firstName,
|
|
234
|
+
},
|
|
235
|
+
active: true,
|
|
236
|
+
emails: [
|
|
237
|
+
{
|
|
238
|
+
value: userData.email,
|
|
239
|
+
type: "work",
|
|
240
|
+
primary: true,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
}
|
|
244
|
+
expect(response).toEqual(expectedScimUser)
|
|
245
|
+
|
|
246
|
+
const persistedUsers = await config.api.scimUsersAPI.get()
|
|
247
|
+
expect(persistedUsers).toEqual(
|
|
248
|
+
expect.objectContaining({
|
|
249
|
+
totalResults: 1,
|
|
250
|
+
Resources: [expectedScimUser],
|
|
251
|
+
})
|
|
252
|
+
)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it("an event is dispatched", async () => {
|
|
256
|
+
const body = structures.scim.createUserRequest()
|
|
257
|
+
|
|
258
|
+
await postScimUser({ body })
|
|
259
|
+
|
|
260
|
+
expect(events.user.created).toBeCalledTimes(1)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe("GET /api/global/scim/v2/users/:id", () => {
|
|
266
|
+
let user: ScimUserResponse
|
|
267
|
+
|
|
268
|
+
beforeAll(async () => {
|
|
269
|
+
const body = structures.scim.createUserRequest()
|
|
270
|
+
|
|
271
|
+
user = await config.api.scimUsersAPI.post({ body })
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const findScimUser = config.api.scimUsersAPI.find
|
|
275
|
+
|
|
276
|
+
unauthorisedTests(findScimUser)
|
|
277
|
+
|
|
278
|
+
it("should return existing user", async () => {
|
|
279
|
+
const response = await findScimUser(user.id)
|
|
280
|
+
|
|
281
|
+
expect(response).toEqual(user)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it("should return 404 when requesting unexisting user id", async () => {
|
|
285
|
+
const response = await findScimUser(structures.uuid(), { expect: 404 })
|
|
286
|
+
|
|
287
|
+
expect(response).toEqual({
|
|
288
|
+
message: "missing",
|
|
289
|
+
status: 404,
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe("PATCH /api/global/scim/v2/users/:id", () => {
|
|
295
|
+
const patchScimUser = config.api.scimUsersAPI.patch
|
|
296
|
+
|
|
297
|
+
let user: ScimUserResponse
|
|
298
|
+
|
|
299
|
+
beforeEach(async () => {
|
|
300
|
+
const body = structures.scim.createUserRequest()
|
|
301
|
+
|
|
302
|
+
user = await config.api.scimUsersAPI.post({ body })
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
unauthorisedTests(patchScimUser)
|
|
306
|
+
|
|
307
|
+
it("an existing user can be updated", async () => {
|
|
308
|
+
const newUserName = structures.generator.name()
|
|
309
|
+
const newFamilyName = structures.generator.last()
|
|
310
|
+
const body: ScimUpdateRequest = {
|
|
311
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
312
|
+
Operations: [
|
|
313
|
+
{
|
|
314
|
+
op: "Replace",
|
|
315
|
+
path: "userName",
|
|
316
|
+
value: newUserName,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
op: "Replace",
|
|
320
|
+
path: "name.familyName",
|
|
321
|
+
value: newFamilyName,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const response = await patchScimUser({ id: user.id, body })
|
|
327
|
+
|
|
328
|
+
const expectedScimUser: ScimUserResponse = {
|
|
329
|
+
...user,
|
|
330
|
+
userName: newUserName,
|
|
331
|
+
name: {
|
|
332
|
+
...user.name,
|
|
333
|
+
familyName: newFamilyName,
|
|
334
|
+
formatted: `${user.name.givenName} ${newFamilyName}`,
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
expect(response).toEqual(expectedScimUser)
|
|
338
|
+
|
|
339
|
+
const persistedUser = await config.api.scimUsersAPI.find(user.id)
|
|
340
|
+
expect(persistedUser).toEqual(expectedScimUser)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it.each([false, "false", "False"])(
|
|
344
|
+
"can deactive an active user (sending %s)",
|
|
345
|
+
async activeValue => {
|
|
346
|
+
const body: ScimUpdateRequest = {
|
|
347
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
348
|
+
Operations: [{ op: "Replace", path: "active", value: activeValue }],
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const response = await patchScimUser({ id: user.id, body })
|
|
352
|
+
|
|
353
|
+
const expectedScimUser: ScimUserResponse = {
|
|
354
|
+
...user,
|
|
355
|
+
active: false,
|
|
356
|
+
}
|
|
357
|
+
expect(response).toEqual(expectedScimUser)
|
|
358
|
+
|
|
359
|
+
const persistedUser = await config.api.scimUsersAPI.find(user.id)
|
|
360
|
+
expect(persistedUser).toEqual(expectedScimUser)
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
it.each([true, "true", "True"])(
|
|
365
|
+
"can activate an inactive user (sending %s)",
|
|
366
|
+
async activeValue => {
|
|
367
|
+
// Deactivate user
|
|
368
|
+
await patchScimUser({
|
|
369
|
+
id: user.id,
|
|
370
|
+
body: {
|
|
371
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
372
|
+
Operations: [{ op: "Replace", path: "active", value: true }],
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
const body: ScimUpdateRequest = {
|
|
377
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
378
|
+
Operations: [{ op: "Replace", path: "active", value: activeValue }],
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const response = await patchScimUser({ id: user.id, body })
|
|
382
|
+
|
|
383
|
+
const expectedScimUser: ScimUserResponse = {
|
|
384
|
+
...user,
|
|
385
|
+
active: true,
|
|
386
|
+
}
|
|
387
|
+
expect(response).toEqual(expectedScimUser)
|
|
388
|
+
|
|
389
|
+
const persistedUser = await config.api.scimUsersAPI.find(user.id)
|
|
390
|
+
expect(persistedUser).toEqual(expectedScimUser)
|
|
391
|
+
}
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
it("supports updating unmapped fields", async () => {
|
|
395
|
+
const body: ScimUpdateRequest = {
|
|
396
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
397
|
+
Operations: [
|
|
398
|
+
{
|
|
399
|
+
op: "Add",
|
|
400
|
+
path: "preferredLanguage",
|
|
401
|
+
value: structures.generator.letter(),
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const response = await patchScimUser({ id: user.id, body })
|
|
407
|
+
|
|
408
|
+
const expectedScimUser: ScimUserResponse = {
|
|
409
|
+
...user,
|
|
410
|
+
}
|
|
411
|
+
expect(response).toEqual(expectedScimUser)
|
|
412
|
+
|
|
413
|
+
const persistedUser = await config.api.scimUsersAPI.find(user.id)
|
|
414
|
+
expect(persistedUser).toEqual(expectedScimUser)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it("an event is dispatched", async () => {
|
|
418
|
+
const body: ScimUpdateRequest = {
|
|
419
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
420
|
+
Operations: [
|
|
421
|
+
{
|
|
422
|
+
op: "Replace",
|
|
423
|
+
path: "userName",
|
|
424
|
+
value: structures.generator.name(),
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
await patchScimUser({ id: user.id, body })
|
|
430
|
+
|
|
431
|
+
expect(events.user.updated).toBeCalledTimes(1)
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
describe("DELETE /api/global/scim/v2/users/:id", () => {
|
|
436
|
+
const deleteScimUser = config.api.scimUsersAPI.delete
|
|
437
|
+
|
|
438
|
+
let user: ScimUserResponse
|
|
439
|
+
|
|
440
|
+
beforeEach(async () => {
|
|
441
|
+
const body = structures.scim.createUserRequest()
|
|
442
|
+
|
|
443
|
+
user = await config.api.scimUsersAPI.post({ body })
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
unauthorisedTests(deleteScimUser)
|
|
447
|
+
|
|
448
|
+
it("an existing user can be deleted", async () => {
|
|
449
|
+
const response = await deleteScimUser(user.id, { expect: 204 })
|
|
450
|
+
|
|
451
|
+
expect(response).toEqual({})
|
|
452
|
+
|
|
453
|
+
await config.api.scimUsersAPI.find(user.id, { expect: 404 })
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it("an non existing user can not be deleted", async () => {
|
|
457
|
+
await deleteScimUser(structures.uuid(), { expect: 404 })
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it("an event is dispatched", async () => {
|
|
461
|
+
await deleteScimUser(user.id, { expect: 204 })
|
|
462
|
+
|
|
463
|
+
expect(events.user.deleted).toBeCalledTimes(1)
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
describe("/api/global/scim/v2/groups", () => {
|
|
469
|
+
describe("GET /api/global/scim/v2/groups", () => {
|
|
470
|
+
const getScimGroups = config.api.scimGroupsAPI.get
|
|
471
|
+
|
|
472
|
+
unauthorisedTests(getScimGroups)
|
|
473
|
+
|
|
474
|
+
describe("no groups exist", () => {
|
|
475
|
+
it("should retrieve empty list", async () => {
|
|
476
|
+
const response = await getScimGroups()
|
|
477
|
+
|
|
478
|
+
expect(response).toEqual({
|
|
479
|
+
Resources: [],
|
|
480
|
+
itemsPerPage: 0,
|
|
481
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
482
|
+
startIndex: 1,
|
|
483
|
+
totalResults: 0,
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
describe("multiple groups exist", () => {
|
|
489
|
+
const groupCount = 25
|
|
490
|
+
let groups: ScimGroupResponse[]
|
|
491
|
+
|
|
492
|
+
beforeAll(async () => {
|
|
493
|
+
groups = []
|
|
494
|
+
|
|
495
|
+
for (let i = 0; i < groupCount; i++) {
|
|
496
|
+
const body = structures.scim.createGroupRequest()
|
|
497
|
+
groups.push(await config.api.scimGroupsAPI.post({ body }))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
groups = groups.sort((a, b) => (a.id > b.id ? 1 : -1))
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it("can fetch all groups without filters", async () => {
|
|
504
|
+
const response = await getScimGroups()
|
|
505
|
+
|
|
506
|
+
expect(response).toEqual({
|
|
507
|
+
Resources: expect.arrayContaining(groups),
|
|
508
|
+
itemsPerPage: 25,
|
|
509
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
510
|
+
startIndex: 1,
|
|
511
|
+
totalResults: groupCount,
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
describe("POST /api/global/scim/v2/groups", () => {
|
|
518
|
+
const postScimGroup = config.api.scimGroupsAPI.post
|
|
519
|
+
|
|
520
|
+
beforeAll(async () => {
|
|
521
|
+
await config.useNewTenant()
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
unauthorisedTests(postScimGroup)
|
|
525
|
+
|
|
526
|
+
describe("no groups exist", () => {
|
|
527
|
+
it("a new group can be created and persisted", async () => {
|
|
528
|
+
const mockedTime = new Date(structures.generator.timestamp())
|
|
529
|
+
tk.freeze(mockedTime)
|
|
530
|
+
|
|
531
|
+
const groupData = {
|
|
532
|
+
externalId: structures.uuid(),
|
|
533
|
+
displayName: structures.generator.word(),
|
|
534
|
+
}
|
|
535
|
+
const body = structures.scim.createGroupRequest(groupData)
|
|
536
|
+
|
|
537
|
+
const response = await postScimGroup({ body })
|
|
538
|
+
|
|
539
|
+
const expectedScimGroup = {
|
|
540
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
|
541
|
+
id: expect.any(String),
|
|
542
|
+
externalId: groupData.externalId,
|
|
543
|
+
displayName: groupData.displayName,
|
|
544
|
+
meta: {
|
|
545
|
+
resourceType: "Group",
|
|
546
|
+
created: mockedTime.toISOString(),
|
|
547
|
+
lastModified: mockedTime.toISOString(),
|
|
548
|
+
},
|
|
549
|
+
members: [],
|
|
550
|
+
}
|
|
551
|
+
expect(response).toEqual(expectedScimGroup)
|
|
552
|
+
|
|
553
|
+
const persistedGroups = await config.api.scimGroupsAPI.get()
|
|
554
|
+
expect(persistedGroups).toEqual(
|
|
555
|
+
expect.objectContaining({
|
|
556
|
+
totalResults: 1,
|
|
557
|
+
Resources: [expectedScimGroup],
|
|
558
|
+
})
|
|
559
|
+
)
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
describe("GET /api/global/scim/v2/groups/:id", () => {
|
|
565
|
+
let group: ScimGroupResponse
|
|
566
|
+
|
|
567
|
+
beforeAll(async () => {
|
|
568
|
+
const body = structures.scim.createGroupRequest()
|
|
569
|
+
|
|
570
|
+
group = await config.api.scimGroupsAPI.post({ body })
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const findScimGroup = config.api.scimGroupsAPI.find
|
|
574
|
+
|
|
575
|
+
unauthorisedTests(findScimGroup)
|
|
576
|
+
|
|
577
|
+
it("should return existing group", async () => {
|
|
578
|
+
const response = await findScimGroup(group.id)
|
|
579
|
+
|
|
580
|
+
expect(response).toEqual(group)
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it("should return 404 when requesting unexisting group id", async () => {
|
|
584
|
+
const response = await findScimGroup(structures.uuid(), { expect: 404 })
|
|
585
|
+
|
|
586
|
+
expect(response).toEqual({
|
|
587
|
+
message: "missing",
|
|
588
|
+
status: 404,
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
describe("DELETE /api/global/scim/v2/groups/:id", () => {
|
|
594
|
+
const deleteScimGroup = config.api.scimGroupsAPI.delete
|
|
595
|
+
|
|
596
|
+
let group: ScimGroupResponse
|
|
597
|
+
|
|
598
|
+
beforeAll(async () => {
|
|
599
|
+
const body = structures.scim.createGroupRequest()
|
|
600
|
+
|
|
601
|
+
group = await config.api.scimGroupsAPI.post({ body })
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
unauthorisedTests(deleteScimGroup)
|
|
605
|
+
|
|
606
|
+
it("an existing group can be deleted", async () => {
|
|
607
|
+
const response = await deleteScimGroup(group.id, { expect: 204 })
|
|
608
|
+
|
|
609
|
+
expect(response).toEqual({})
|
|
610
|
+
|
|
611
|
+
await config.api.scimGroupsAPI.find(group.id, { expect: 404 })
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it("an non existing group can not be deleted", async () => {
|
|
615
|
+
await deleteScimGroup(structures.uuid(), { expect: 404 })
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
describe("PATCH /api/global/scim/v2/groups/:id", () => {
|
|
620
|
+
const patchScimGroup = config.api.scimGroupsAPI.patch
|
|
621
|
+
|
|
622
|
+
let group: ScimGroupResponse
|
|
623
|
+
let users: ScimUserResponse[]
|
|
624
|
+
|
|
625
|
+
beforeAll(async () => {
|
|
626
|
+
users = []
|
|
627
|
+
for (let i = 0; i < 30; i++) {
|
|
628
|
+
const body = structures.scim.createUserRequest()
|
|
629
|
+
users.push(await config.api.scimUsersAPI.post({ body }))
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
users = users.sort((a, b) => (a.id > b.id ? 1 : -1))
|
|
633
|
+
|
|
634
|
+
const body = structures.scim.createGroupRequest()
|
|
635
|
+
group = await config.api.scimGroupsAPI.post({ body })
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
unauthorisedTests(patchScimGroup)
|
|
639
|
+
|
|
640
|
+
it("an existing group can be updated", async () => {
|
|
641
|
+
const newDisplayName = structures.generator.word()
|
|
642
|
+
|
|
643
|
+
const body: ScimUpdateRequest = {
|
|
644
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
645
|
+
Operations: [
|
|
646
|
+
{
|
|
647
|
+
op: "Replace",
|
|
648
|
+
path: "displayName",
|
|
649
|
+
value: newDisplayName,
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const response = await patchScimGroup({ id: group.id, body })
|
|
655
|
+
|
|
656
|
+
const expectedScimGroup = {
|
|
657
|
+
...group,
|
|
658
|
+
displayName: newDisplayName,
|
|
659
|
+
}
|
|
660
|
+
expect(response).toEqual(expectedScimGroup)
|
|
661
|
+
|
|
662
|
+
const persistedGroup = await config.api.scimGroupsAPI.find(group.id)
|
|
663
|
+
expect(persistedGroup).toEqual(expectedScimGroup)
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
describe("adding users", () => {
|
|
667
|
+
beforeAll(async () => {
|
|
668
|
+
const body = structures.scim.createGroupRequest()
|
|
669
|
+
group = await config.api.scimGroupsAPI.post({ body })
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
it("a new user can be added to an existing group", async () => {
|
|
673
|
+
const userToAdd = users[0]
|
|
674
|
+
|
|
675
|
+
const body: ScimUpdateRequest = {
|
|
676
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
677
|
+
Operations: [
|
|
678
|
+
{
|
|
679
|
+
op: "Add",
|
|
680
|
+
path: "members",
|
|
681
|
+
value: [
|
|
682
|
+
{
|
|
683
|
+
$ref: null,
|
|
684
|
+
value: userToAdd.id,
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
},
|
|
688
|
+
],
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const response = await patchScimGroup({ id: group.id, body })
|
|
692
|
+
|
|
693
|
+
const expectedScimGroup: ScimGroupResponse = {
|
|
694
|
+
...group,
|
|
695
|
+
members: [
|
|
696
|
+
{
|
|
697
|
+
value: userToAdd.id,
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
}
|
|
701
|
+
expect(response).toEqual(expectedScimGroup)
|
|
702
|
+
|
|
703
|
+
const persistedGroup = await config.api.scimGroupsAPI.find(group.id)
|
|
704
|
+
expect(persistedGroup).toEqual(expectedScimGroup)
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
it("multiple users can be added to an existing group", async () => {
|
|
708
|
+
const body: ScimUpdateRequest = {
|
|
709
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
710
|
+
Operations: [
|
|
711
|
+
{
|
|
712
|
+
op: "Add",
|
|
713
|
+
path: "members",
|
|
714
|
+
value: [
|
|
715
|
+
{
|
|
716
|
+
$ref: null,
|
|
717
|
+
value: users[1].id,
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
$ref: null,
|
|
721
|
+
value: users[2].id,
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
$ref: null,
|
|
725
|
+
value: users[3].id,
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const response = await patchScimGroup({ id: group.id, body })
|
|
733
|
+
|
|
734
|
+
const expectedScimGroup: ScimGroupResponse = {
|
|
735
|
+
...group,
|
|
736
|
+
members: [
|
|
737
|
+
{
|
|
738
|
+
value: users[0].id,
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
value: users[1].id,
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
value: users[2].id,
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
value: users[3].id,
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
}
|
|
751
|
+
expect(response).toEqual(expectedScimGroup)
|
|
752
|
+
|
|
753
|
+
const persistedGroup = await config.api.scimGroupsAPI.find(group.id)
|
|
754
|
+
expect(persistedGroup).toEqual(expectedScimGroup)
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it("existing users can be removed from to an existing group", async () => {
|
|
758
|
+
const body: ScimUpdateRequest = {
|
|
759
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
760
|
+
Operations: [
|
|
761
|
+
{
|
|
762
|
+
op: "Remove",
|
|
763
|
+
path: "members",
|
|
764
|
+
value: [
|
|
765
|
+
{
|
|
766
|
+
$ref: null,
|
|
767
|
+
value: users[0].id,
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
$ref: null,
|
|
771
|
+
value: users[2].id,
|
|
772
|
+
},
|
|
773
|
+
],
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const response = await patchScimGroup({ id: group.id, body })
|
|
779
|
+
|
|
780
|
+
const expectedScimGroup: ScimGroupResponse = {
|
|
781
|
+
...group,
|
|
782
|
+
members: expect.arrayContaining([
|
|
783
|
+
{
|
|
784
|
+
value: users[1].id,
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
value: users[3].id,
|
|
788
|
+
},
|
|
789
|
+
]),
|
|
790
|
+
}
|
|
791
|
+
expect(response).toEqual(expectedScimGroup)
|
|
792
|
+
|
|
793
|
+
const persistedGroup = await config.api.scimGroupsAPI.find(group.id)
|
|
794
|
+
expect(persistedGroup).toEqual(expectedScimGroup)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it("adding and removing can be added in a single operation", async () => {
|
|
798
|
+
const body: ScimUpdateRequest = {
|
|
799
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
800
|
+
Operations: [
|
|
801
|
+
{
|
|
802
|
+
op: "Remove",
|
|
803
|
+
path: "members",
|
|
804
|
+
value: [
|
|
805
|
+
{
|
|
806
|
+
$ref: null,
|
|
807
|
+
value: users[1].id,
|
|
808
|
+
},
|
|
809
|
+
],
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
op: "Add",
|
|
813
|
+
path: "members",
|
|
814
|
+
value: [
|
|
815
|
+
{
|
|
816
|
+
$ref: null,
|
|
817
|
+
value: users[4].id,
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const response = await patchScimGroup({ id: group.id, body })
|
|
825
|
+
|
|
826
|
+
const expectedScimGroup: ScimGroupResponse = {
|
|
827
|
+
...group,
|
|
828
|
+
members: expect.arrayContaining([
|
|
829
|
+
{
|
|
830
|
+
value: users[3].id,
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
value: users[4].id,
|
|
834
|
+
},
|
|
835
|
+
]),
|
|
836
|
+
}
|
|
837
|
+
expect(response).toEqual(expectedScimGroup)
|
|
838
|
+
|
|
839
|
+
const persistedGroup = await config.api.scimGroupsAPI.find(group.id)
|
|
840
|
+
expect(persistedGroup).toEqual(expectedScimGroup)
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
it("adding members and updating fields can performed in a single operation", async () => {
|
|
844
|
+
const newDisplayName = structures.generator.word()
|
|
845
|
+
|
|
846
|
+
const body: ScimUpdateRequest = {
|
|
847
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
848
|
+
Operations: [
|
|
849
|
+
{
|
|
850
|
+
op: "Replace",
|
|
851
|
+
path: "displayName",
|
|
852
|
+
value: newDisplayName,
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
op: "Add",
|
|
856
|
+
path: "members",
|
|
857
|
+
value: [
|
|
858
|
+
{
|
|
859
|
+
$ref: null,
|
|
860
|
+
value: users[5].id,
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const response = await patchScimGroup({ id: group.id, body })
|
|
868
|
+
|
|
869
|
+
const expectedScimGroup: ScimGroupResponse = {
|
|
870
|
+
...group,
|
|
871
|
+
displayName: newDisplayName,
|
|
872
|
+
members: expect.arrayContaining([
|
|
873
|
+
{
|
|
874
|
+
value: users[3].id,
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
value: users[4].id,
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
value: users[5].id,
|
|
881
|
+
},
|
|
882
|
+
]),
|
|
883
|
+
}
|
|
884
|
+
expect(response).toEqual(expectedScimGroup)
|
|
885
|
+
|
|
886
|
+
const persistedGroup = await config.api.scimGroupsAPI.find(group.id)
|
|
887
|
+
expect(persistedGroup).toEqual(expectedScimGroup)
|
|
888
|
+
})
|
|
889
|
+
})
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
})
|