@better-auth/scim 1.5.0-beta.18 → 1.5.0-beta.19
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/README.md +17 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +5 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -16
- package/.turbo/turbo-build.log +0 -19
- package/src/client.ts +0 -9
- package/src/index.ts +0 -90
- package/src/mappings.ts +0 -38
- package/src/middlewares.ts +0 -89
- package/src/patch-operations.ts +0 -148
- package/src/routes.ts +0 -1268
- package/src/scim-error.ts +0 -99
- package/src/scim-filters.ts +0 -69
- package/src/scim-metadata.ts +0 -128
- package/src/scim-resources.ts +0 -35
- package/src/scim-tokens.ts +0 -71
- package/src/scim.management.test.ts +0 -810
- package/src/scim.test.ts +0 -2357
- package/src/types.ts +0 -78
- package/src/user-schemas.ts +0 -213
- package/src/utils.ts +0 -5
- package/tsconfig.json +0 -11
- package/tsdown.config.ts +0 -8
- package/vitest.config.ts +0 -8
|
@@ -1,810 +0,0 @@
|
|
|
1
|
-
import { sso } from "@better-auth/sso";
|
|
2
|
-
import { APIError, betterAuth } from "better-auth";
|
|
3
|
-
import { memoryAdapter } from "better-auth/adapters/memory";
|
|
4
|
-
import { createAuthClient } from "better-auth/client";
|
|
5
|
-
import { setCookieToHeader } from "better-auth/cookies";
|
|
6
|
-
import { bearer, organization } from "better-auth/plugins";
|
|
7
|
-
import { describe, expect, it } from "vitest";
|
|
8
|
-
import { scim } from ".";
|
|
9
|
-
import { scimClient } from "./client";
|
|
10
|
-
import type { SCIMOptions } from "./types";
|
|
11
|
-
|
|
12
|
-
const createTestInstance = (scimOptions?: SCIMOptions) => {
|
|
13
|
-
const testUser = {
|
|
14
|
-
email: "test@email.com",
|
|
15
|
-
password: "password",
|
|
16
|
-
name: "Test User",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const data = {
|
|
20
|
-
user: [],
|
|
21
|
-
session: [],
|
|
22
|
-
verification: [],
|
|
23
|
-
account: [],
|
|
24
|
-
ssoProvider: [],
|
|
25
|
-
scimProvider: [],
|
|
26
|
-
organization: [],
|
|
27
|
-
member: [],
|
|
28
|
-
};
|
|
29
|
-
const memory = memoryAdapter(data);
|
|
30
|
-
|
|
31
|
-
const auth = betterAuth({
|
|
32
|
-
database: memory,
|
|
33
|
-
baseURL: "http://localhost:3000",
|
|
34
|
-
emailAndPassword: {
|
|
35
|
-
enabled: true,
|
|
36
|
-
},
|
|
37
|
-
plugins: [sso(), scim(scimOptions), organization()],
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const authClient = createAuthClient({
|
|
41
|
-
baseURL: "http://localhost:3000",
|
|
42
|
-
plugins: [bearer(), scimClient()],
|
|
43
|
-
fetchOptions: {
|
|
44
|
-
customFetchImpl: async (url, init) => {
|
|
45
|
-
return auth.handler(new Request(url, init));
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
async function getAuthCookieHeaders(
|
|
51
|
-
user: { email: string; password: string; name: string } = testUser,
|
|
52
|
-
) {
|
|
53
|
-
const headers = new Headers();
|
|
54
|
-
|
|
55
|
-
await authClient.signUp.email({
|
|
56
|
-
email: user.email,
|
|
57
|
-
password: user.password,
|
|
58
|
-
name: user.name,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await authClient.signIn.email(user, {
|
|
62
|
-
throw: true,
|
|
63
|
-
onSuccess: setCookieToHeader(headers),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return headers;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function getSCIMToken(
|
|
70
|
-
providerId: string = "the-saml-provider-1",
|
|
71
|
-
organizationId?: string,
|
|
72
|
-
userHeaders?: Headers,
|
|
73
|
-
) {
|
|
74
|
-
const headers = userHeaders ?? (await getAuthCookieHeaders());
|
|
75
|
-
const { scimToken } = await auth.api.generateSCIMToken({
|
|
76
|
-
body: {
|
|
77
|
-
providerId,
|
|
78
|
-
organizationId,
|
|
79
|
-
},
|
|
80
|
-
headers,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
return scimToken;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function registerOrganization(org: string, userHeaders?: Headers) {
|
|
87
|
-
const headers = userHeaders ?? (await getAuthCookieHeaders());
|
|
88
|
-
return await auth.api.createOrganization({
|
|
89
|
-
body: {
|
|
90
|
-
slug: `the-${org}`,
|
|
91
|
-
name: `the organization ${org}`,
|
|
92
|
-
},
|
|
93
|
-
headers,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
auth,
|
|
99
|
-
authClient,
|
|
100
|
-
registerOrganization,
|
|
101
|
-
getSCIMToken,
|
|
102
|
-
getAuthCookieHeaders,
|
|
103
|
-
};
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const policyUserA = {
|
|
107
|
-
email: "user1@policy.test",
|
|
108
|
-
password: "password",
|
|
109
|
-
name: "User One",
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const policyUserB = {
|
|
113
|
-
email: "user2@policy.test",
|
|
114
|
-
password: "password",
|
|
115
|
-
name: "User Two",
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
describe("SCIM provider management", () => {
|
|
119
|
-
describe("POST /scim/generate-token", () => {
|
|
120
|
-
it("should require user session", async () => {
|
|
121
|
-
const { auth } = createTestInstance();
|
|
122
|
-
const generateSCIMToken = () =>
|
|
123
|
-
auth.api.generateSCIMToken({ body: { providerId: "the id" } });
|
|
124
|
-
|
|
125
|
-
await expect(generateSCIMToken()).rejects.toThrowError(
|
|
126
|
-
expect.objectContaining({
|
|
127
|
-
status: "UNAUTHORIZED",
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("should fail if the authenticated user does not belong to the given org", async () => {
|
|
133
|
-
const { auth, getAuthCookieHeaders } = createTestInstance();
|
|
134
|
-
const headers = await getAuthCookieHeaders();
|
|
135
|
-
const generateSCIMToken = () =>
|
|
136
|
-
auth.api.generateSCIMToken({
|
|
137
|
-
body: { providerId: "the id", organizationId: "the-org" },
|
|
138
|
-
headers,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
await expect(generateSCIMToken()).rejects.toThrowError(
|
|
142
|
-
expect.objectContaining({
|
|
143
|
-
message: "You are not a member of the organization",
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("should fail to generate a SCIM token on invalid provider", async () => {
|
|
149
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
150
|
-
storeSCIMToken: "plain",
|
|
151
|
-
});
|
|
152
|
-
const headers = await getAuthCookieHeaders();
|
|
153
|
-
|
|
154
|
-
const generateSCIMToken = (providerId: string, organizationId?: string) =>
|
|
155
|
-
auth.api.generateSCIMToken({
|
|
156
|
-
body: { providerId, organizationId },
|
|
157
|
-
headers,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
await expect(generateSCIMToken("the:provider")).rejects.toThrowError(
|
|
161
|
-
expect.objectContaining({
|
|
162
|
-
message: "Provider id contains forbidden characters",
|
|
163
|
-
}),
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("should generate a new scim token (client)", async () => {
|
|
168
|
-
const { auth, authClient, getAuthCookieHeaders } = createTestInstance();
|
|
169
|
-
|
|
170
|
-
const headers = await getAuthCookieHeaders();
|
|
171
|
-
const response = await authClient.scim.generateToken(
|
|
172
|
-
{
|
|
173
|
-
providerId: "the id",
|
|
174
|
-
},
|
|
175
|
-
{ headers },
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
expect(response.data).toMatchObject({
|
|
179
|
-
scimToken: expect.any(String),
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const createUser = () =>
|
|
183
|
-
auth.api.createSCIMUser({
|
|
184
|
-
body: {
|
|
185
|
-
userName: "the-username",
|
|
186
|
-
},
|
|
187
|
-
headers: {
|
|
188
|
-
authorization: `Bearer ${response.data?.scimToken}`,
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
await expect(createUser()).resolves.toBeTruthy();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("should generate a new scim token (plain)", async () => {
|
|
196
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
197
|
-
storeSCIMToken: "plain",
|
|
198
|
-
});
|
|
199
|
-
const headers = await getAuthCookieHeaders();
|
|
200
|
-
|
|
201
|
-
const response = await auth.api.generateSCIMToken({
|
|
202
|
-
body: { providerId: "the id" },
|
|
203
|
-
headers,
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
expect(response).toMatchObject({
|
|
207
|
-
scimToken: expect.any(String),
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const createUser = () =>
|
|
211
|
-
auth.api.createSCIMUser({
|
|
212
|
-
body: {
|
|
213
|
-
userName: "the-username",
|
|
214
|
-
},
|
|
215
|
-
headers: {
|
|
216
|
-
authorization: `Bearer ${response.scimToken}`,
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
await expect(createUser()).resolves.toBeTruthy();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("should generate a new scim token (hashed)", async () => {
|
|
224
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
225
|
-
storeSCIMToken: "hashed",
|
|
226
|
-
});
|
|
227
|
-
const headers = await getAuthCookieHeaders();
|
|
228
|
-
|
|
229
|
-
const response = await auth.api.generateSCIMToken({
|
|
230
|
-
body: { providerId: "the id" },
|
|
231
|
-
headers,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
expect(response).toMatchObject({
|
|
235
|
-
scimToken: expect.any(String),
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const createUser = () =>
|
|
239
|
-
auth.api.createSCIMUser({
|
|
240
|
-
body: {
|
|
241
|
-
userName: "the-username",
|
|
242
|
-
},
|
|
243
|
-
headers: {
|
|
244
|
-
authorization: `Bearer ${response.scimToken}`,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
await expect(createUser()).resolves.toBeTruthy();
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("should generate a new scim token (custom hash)", async () => {
|
|
252
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
253
|
-
storeSCIMToken: { hash: async (value) => value + "hello" },
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
const headers = await getAuthCookieHeaders();
|
|
257
|
-
const response = await auth.api.generateSCIMToken({
|
|
258
|
-
body: { providerId: "the id" },
|
|
259
|
-
headers,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const createUser = () =>
|
|
263
|
-
auth.api.createSCIMUser({
|
|
264
|
-
body: {
|
|
265
|
-
userName: "the-username",
|
|
266
|
-
},
|
|
267
|
-
headers: {
|
|
268
|
-
authorization: `Bearer ${response.scimToken}`,
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
await expect(createUser()).resolves.toBeTruthy();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("should generate a new scim token (encrypted)", async () => {
|
|
276
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
277
|
-
storeSCIMToken: "encrypted",
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const headers = await getAuthCookieHeaders();
|
|
281
|
-
const response = await auth.api.generateSCIMToken({
|
|
282
|
-
body: { providerId: "the id" },
|
|
283
|
-
headers,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const createUser = () =>
|
|
287
|
-
auth.api.createSCIMUser({
|
|
288
|
-
body: {
|
|
289
|
-
userName: "the-username",
|
|
290
|
-
},
|
|
291
|
-
headers: {
|
|
292
|
-
authorization: `Bearer ${response.scimToken}`,
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
await expect(createUser()).resolves.toBeTruthy();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("should generate a new scim token (custom encryption)", async () => {
|
|
300
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
301
|
-
storeSCIMToken: {
|
|
302
|
-
encrypt: async (value) => value,
|
|
303
|
-
decrypt: async (value) => value,
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const headers = await getAuthCookieHeaders();
|
|
308
|
-
const response = await auth.api.generateSCIMToken({
|
|
309
|
-
body: { providerId: "the id" },
|
|
310
|
-
headers,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
const createUser = () =>
|
|
314
|
-
auth.api.createSCIMUser({
|
|
315
|
-
body: {
|
|
316
|
-
userName: "the-username",
|
|
317
|
-
},
|
|
318
|
-
headers: {
|
|
319
|
-
authorization: `Bearer ${response.scimToken}`,
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
await expect(createUser()).resolves.toBeTruthy();
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("should generate a new scim token associated to an org", async () => {
|
|
327
|
-
const { auth, registerOrganization, getAuthCookieHeaders } =
|
|
328
|
-
createTestInstance();
|
|
329
|
-
const orgA = await registerOrganization("org-a");
|
|
330
|
-
const headers = await getAuthCookieHeaders();
|
|
331
|
-
|
|
332
|
-
const response = await auth.api.generateSCIMToken({
|
|
333
|
-
body: { providerId: "the id", organizationId: orgA?.id },
|
|
334
|
-
headers,
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
expect(response).toMatchObject({
|
|
338
|
-
scimToken: expect.any(String),
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("should execute hooks before SCIM token generation", async () => {
|
|
343
|
-
const { auth, getAuthCookieHeaders, registerOrganization } =
|
|
344
|
-
createTestInstance({
|
|
345
|
-
beforeSCIMTokenGenerated: async ({ user, member, scimToken }) => {
|
|
346
|
-
if (member?.role === "owner") {
|
|
347
|
-
throw new APIError("FORBIDDEN", {
|
|
348
|
-
message:
|
|
349
|
-
"You do not have enough privileges to generate a SCIM token",
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
const headers = await getAuthCookieHeaders();
|
|
355
|
-
const orgA = await registerOrganization("the org");
|
|
356
|
-
|
|
357
|
-
const generateSCIMToken = () =>
|
|
358
|
-
auth.api.generateSCIMToken({
|
|
359
|
-
body: { providerId: "the id", organizationId: orgA?.id },
|
|
360
|
-
headers,
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
await expect(generateSCIMToken()).rejects.toThrowError(
|
|
364
|
-
expect.objectContaining({
|
|
365
|
-
message: "You do not have enough privileges to generate a SCIM token",
|
|
366
|
-
}),
|
|
367
|
-
);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("should execute hooks after SCIM token generation", async () => {
|
|
371
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
372
|
-
storeSCIMToken: "plain",
|
|
373
|
-
afterSCIMTokenGenerated: async ({
|
|
374
|
-
user,
|
|
375
|
-
member,
|
|
376
|
-
scimProvider,
|
|
377
|
-
scimToken,
|
|
378
|
-
}) => {
|
|
379
|
-
expect(scimProvider.scimToken).toBeTypeOf("string");
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
const headers = await getAuthCookieHeaders();
|
|
383
|
-
|
|
384
|
-
const response = await auth.api.generateSCIMToken({
|
|
385
|
-
body: { providerId: "the id" },
|
|
386
|
-
headers,
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
expect(response).toMatchObject({
|
|
390
|
-
scimToken: expect.any(String),
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it("should deny regenerate when user is not the owner of a personal provider", async () => {
|
|
395
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
396
|
-
providerOwnership: { enabled: true },
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
const [headersUserA, headersUserB] = await Promise.all([
|
|
400
|
-
getAuthCookieHeaders(policyUserA),
|
|
401
|
-
getAuthCookieHeaders(policyUserB),
|
|
402
|
-
]);
|
|
403
|
-
|
|
404
|
-
await auth.api.generateSCIMToken({
|
|
405
|
-
body: { providerId: "user-a-owned-provider" },
|
|
406
|
-
headers: headersUserA,
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
await expect(
|
|
410
|
-
auth.api.generateSCIMToken({
|
|
411
|
-
body: { providerId: "user-a-owned-provider" },
|
|
412
|
-
headers: headersUserB,
|
|
413
|
-
}),
|
|
414
|
-
).rejects.toMatchObject({
|
|
415
|
-
status: "FORBIDDEN",
|
|
416
|
-
message: "You must be the owner to access this provider",
|
|
417
|
-
});
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("should deny regenerate when provider belongs to another org", async () => {
|
|
421
|
-
const { auth, getAuthCookieHeaders, registerOrganization } =
|
|
422
|
-
createTestInstance();
|
|
423
|
-
|
|
424
|
-
const [headers1, headers2] = await Promise.all([
|
|
425
|
-
getAuthCookieHeaders(policyUserA),
|
|
426
|
-
getAuthCookieHeaders(policyUserB),
|
|
427
|
-
]);
|
|
428
|
-
|
|
429
|
-
const [org1, _org2] = await Promise.all([
|
|
430
|
-
registerOrganization("policy-org-1", headers1),
|
|
431
|
-
registerOrganization("policy-org-2", headers2),
|
|
432
|
-
]);
|
|
433
|
-
|
|
434
|
-
await auth.api.generateSCIMToken({
|
|
435
|
-
body: { providerId: "other-org", organizationId: org1?.id },
|
|
436
|
-
headers: headers1,
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// User B omits organizationId - tries to replace org1's provider
|
|
440
|
-
await expect(
|
|
441
|
-
auth.api.generateSCIMToken({
|
|
442
|
-
body: { providerId: "other-org" },
|
|
443
|
-
headers: headers2,
|
|
444
|
-
}),
|
|
445
|
-
).rejects.toMatchObject({
|
|
446
|
-
status: "FORBIDDEN",
|
|
447
|
-
message:
|
|
448
|
-
"You must be a member of the organization to access this provider",
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
describe("GET /scim/list-provider-connections", () => {
|
|
454
|
-
it("should return empty list when user is not in any org", async () => {
|
|
455
|
-
const { auth, getAuthCookieHeaders } = createTestInstance();
|
|
456
|
-
const headers = await getAuthCookieHeaders();
|
|
457
|
-
|
|
458
|
-
const res = await auth.api.listSCIMProviderConnections({ headers });
|
|
459
|
-
|
|
460
|
-
expect(res).toMatchObject({ providers: [] });
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it("should return org-scoped providers for orgs the user is a member of", async () => {
|
|
464
|
-
const { auth, getAuthCookieHeaders, registerOrganization, getSCIMToken } =
|
|
465
|
-
createTestInstance();
|
|
466
|
-
|
|
467
|
-
const [headersUserA, headersUserB] = await Promise.all([
|
|
468
|
-
getAuthCookieHeaders(policyUserA),
|
|
469
|
-
getAuthCookieHeaders(policyUserB),
|
|
470
|
-
]);
|
|
471
|
-
const [orgA, orgB] = await Promise.all([
|
|
472
|
-
registerOrganization("org-a", headersUserA),
|
|
473
|
-
registerOrganization("org-b", headersUserB),
|
|
474
|
-
]);
|
|
475
|
-
|
|
476
|
-
await Promise.all([
|
|
477
|
-
getSCIMToken("provider-1", orgA!.id, headersUserA),
|
|
478
|
-
getSCIMToken("provider-2", orgA!.id, headersUserA),
|
|
479
|
-
getSCIMToken("provider-3", orgB!.id, headersUserB),
|
|
480
|
-
]);
|
|
481
|
-
|
|
482
|
-
const res = await auth.api.listSCIMProviderConnections({
|
|
483
|
-
headers: headersUserA,
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
expect(res.providers).toHaveLength(2);
|
|
487
|
-
expect(res.providers?.map((p) => p.providerId).sort()).toEqual([
|
|
488
|
-
"provider-1",
|
|
489
|
-
"provider-2",
|
|
490
|
-
]);
|
|
491
|
-
const byProviderId = Object.fromEntries(
|
|
492
|
-
(res.providers ?? []).map((p) => [p.providerId, p]),
|
|
493
|
-
);
|
|
494
|
-
expect(byProviderId["provider-1"]).toMatchObject({
|
|
495
|
-
id: expect.any(String),
|
|
496
|
-
providerId: "provider-1",
|
|
497
|
-
organizationId: orgA!.id,
|
|
498
|
-
});
|
|
499
|
-
expect(byProviderId["provider-2"]).toMatchObject({
|
|
500
|
-
id: expect.any(String),
|
|
501
|
-
providerId: "provider-2",
|
|
502
|
-
organizationId: orgA!.id,
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it("should return owned non-org providers in list for the owner", async () => {
|
|
507
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
508
|
-
providerOwnership: { enabled: true },
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
const [headersUserA, headersUserB] = await Promise.all([
|
|
512
|
-
getAuthCookieHeaders(policyUserA),
|
|
513
|
-
getAuthCookieHeaders(policyUserB),
|
|
514
|
-
]);
|
|
515
|
-
|
|
516
|
-
await auth.api.generateSCIMToken({
|
|
517
|
-
body: { providerId: "user-a-personal-provider" },
|
|
518
|
-
headers: headersUserA,
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
const resUserA = await auth.api.listSCIMProviderConnections({
|
|
522
|
-
headers: headersUserA,
|
|
523
|
-
});
|
|
524
|
-
expect(resUserA.providers).toHaveLength(1);
|
|
525
|
-
expect(resUserA.providers?.[0]).toMatchObject({
|
|
526
|
-
providerId: "user-a-personal-provider",
|
|
527
|
-
organizationId: null,
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
const resUserB = await auth.api.listSCIMProviderConnections({
|
|
531
|
-
headers: headersUserB,
|
|
532
|
-
});
|
|
533
|
-
expect(resUserB.providers).toHaveLength(0);
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
describe("GET /scim/get-provider-connection", () => {
|
|
538
|
-
it("should return provider details when user is org member", async () => {
|
|
539
|
-
const { auth, getAuthCookieHeaders, registerOrganization, getSCIMToken } =
|
|
540
|
-
createTestInstance();
|
|
541
|
-
const headers = await getAuthCookieHeaders();
|
|
542
|
-
|
|
543
|
-
const org = await registerOrganization("scim-get-org");
|
|
544
|
-
await getSCIMToken("my-provider", org!.id);
|
|
545
|
-
|
|
546
|
-
const res = await auth.api.getSCIMProviderConnection({
|
|
547
|
-
query: { providerId: "my-provider" },
|
|
548
|
-
headers,
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
expect(res).toMatchObject({
|
|
552
|
-
id: expect.any(String),
|
|
553
|
-
providerId: "my-provider",
|
|
554
|
-
organizationId: org!.id,
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it("should always return provider when it doesn't belong to an org", async () => {
|
|
559
|
-
const { auth, getAuthCookieHeaders, getSCIMToken } = createTestInstance();
|
|
560
|
-
const headers = await getAuthCookieHeaders();
|
|
561
|
-
|
|
562
|
-
await getSCIMToken("no-org-provider");
|
|
563
|
-
|
|
564
|
-
const res = await auth.api.getSCIMProviderConnection({
|
|
565
|
-
query: { providerId: "no-org-provider" },
|
|
566
|
-
headers,
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
expect(res).toMatchObject({
|
|
570
|
-
providerId: "no-org-provider",
|
|
571
|
-
organizationId: null,
|
|
572
|
-
});
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it("should deny access to non-org provider when user is not the owner", async () => {
|
|
576
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
577
|
-
providerOwnership: { enabled: true },
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
const [headersUserA, headersUserB] = await Promise.all([
|
|
581
|
-
getAuthCookieHeaders(policyUserA),
|
|
582
|
-
getAuthCookieHeaders(policyUserB),
|
|
583
|
-
]);
|
|
584
|
-
|
|
585
|
-
await auth.api.generateSCIMToken({
|
|
586
|
-
body: { providerId: "user-a-owned-provider" },
|
|
587
|
-
headers: headersUserA,
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
await expect(
|
|
591
|
-
auth.api.getSCIMProviderConnection({
|
|
592
|
-
query: { providerId: "user-a-owned-provider" },
|
|
593
|
-
headers: headersUserB,
|
|
594
|
-
}),
|
|
595
|
-
).rejects.toMatchObject({
|
|
596
|
-
status: "FORBIDDEN",
|
|
597
|
-
message: "You must be the owner to access this provider",
|
|
598
|
-
});
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
it("should return 403 when provider belongs to another org", async () => {
|
|
602
|
-
const { auth, getAuthCookieHeaders, registerOrganization } =
|
|
603
|
-
createTestInstance();
|
|
604
|
-
|
|
605
|
-
const [headers1, headers2] = await Promise.all([
|
|
606
|
-
getAuthCookieHeaders(policyUserA),
|
|
607
|
-
getAuthCookieHeaders(policyUserB),
|
|
608
|
-
]);
|
|
609
|
-
|
|
610
|
-
const [org1, _org2] = await Promise.all([
|
|
611
|
-
registerOrganization("get-policy-org-1", headers1),
|
|
612
|
-
registerOrganization("get-policy-org-2", headers2),
|
|
613
|
-
]);
|
|
614
|
-
|
|
615
|
-
await auth.api.generateSCIMToken({
|
|
616
|
-
body: { providerId: "other-org-provider", organizationId: org1?.id },
|
|
617
|
-
headers: headers1,
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
await expect(
|
|
621
|
-
auth.api.getSCIMProviderConnection({
|
|
622
|
-
query: { providerId: "other-org-provider" },
|
|
623
|
-
headers: headers2,
|
|
624
|
-
}),
|
|
625
|
-
).rejects.toMatchObject({
|
|
626
|
-
status: "FORBIDDEN",
|
|
627
|
-
message:
|
|
628
|
-
"You must be a member of the organization to access this provider",
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
it("should return 403 when token creator was removed from org (org membership required)", async () => {
|
|
633
|
-
const { auth, getAuthCookieHeaders, registerOrganization } =
|
|
634
|
-
createTestInstance({ providerOwnership: { enabled: true } });
|
|
635
|
-
|
|
636
|
-
const [headersUserA, headersUserB] = await Promise.all([
|
|
637
|
-
getAuthCookieHeaders(policyUserA),
|
|
638
|
-
getAuthCookieHeaders(policyUserB),
|
|
639
|
-
]);
|
|
640
|
-
|
|
641
|
-
const org = await registerOrganization("owner-removed-org", headersUserA);
|
|
642
|
-
await auth.api.generateSCIMToken({
|
|
643
|
-
body: { providerId: "owner-removed-provider", organizationId: org?.id },
|
|
644
|
-
headers: headersUserA,
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
const sessionB = await auth.api.getSession({ headers: headersUserB });
|
|
648
|
-
if (!sessionB?.user?.id) throw new Error("User B session not found");
|
|
649
|
-
await auth.api.addMember({
|
|
650
|
-
body: {
|
|
651
|
-
organizationId: org!.id,
|
|
652
|
-
userId: sessionB.user.id,
|
|
653
|
-
role: "owner",
|
|
654
|
-
},
|
|
655
|
-
headers: headersUserA,
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
await auth.api.removeMember({
|
|
659
|
-
body: {
|
|
660
|
-
organizationId: org!.id,
|
|
661
|
-
memberIdOrEmail: policyUserA.email,
|
|
662
|
-
},
|
|
663
|
-
headers: headersUserB,
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
await expect(
|
|
667
|
-
auth.api.getSCIMProviderConnection({
|
|
668
|
-
query: { providerId: "owner-removed-provider" },
|
|
669
|
-
headers: headersUserA,
|
|
670
|
-
}),
|
|
671
|
-
).rejects.toMatchObject({
|
|
672
|
-
status: "FORBIDDEN",
|
|
673
|
-
message:
|
|
674
|
-
"You must be a member of the organization to access this provider",
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
const listRes = await auth.api.listSCIMProviderConnections({
|
|
678
|
-
headers: headersUserA,
|
|
679
|
-
});
|
|
680
|
-
expect(
|
|
681
|
-
listRes.providers?.some(
|
|
682
|
-
(p) => p.providerId === "owner-removed-provider",
|
|
683
|
-
),
|
|
684
|
-
).toBe(false);
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
it("should return 404 for unknown providerId", async () => {
|
|
688
|
-
const { auth, getAuthCookieHeaders } = createTestInstance();
|
|
689
|
-
const headers = await getAuthCookieHeaders();
|
|
690
|
-
|
|
691
|
-
await expect(
|
|
692
|
-
auth.api.getSCIMProviderConnection({
|
|
693
|
-
query: { providerId: "unknown" },
|
|
694
|
-
headers,
|
|
695
|
-
}),
|
|
696
|
-
).rejects.toMatchObject({
|
|
697
|
-
message: "SCIM provider not found",
|
|
698
|
-
});
|
|
699
|
-
});
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
describe("POST /scim/delete-provider-connection", () => {
|
|
703
|
-
it("should delete org-scoped provider and invalidate token when user is org member", async () => {
|
|
704
|
-
const { auth, getAuthCookieHeaders, getSCIMToken, registerOrganization } =
|
|
705
|
-
createTestInstance();
|
|
706
|
-
const headers = await getAuthCookieHeaders();
|
|
707
|
-
|
|
708
|
-
const org = await registerOrganization("org-a");
|
|
709
|
-
const scimToken = await getSCIMToken("my-provider", org!.id);
|
|
710
|
-
|
|
711
|
-
const listBefore = await auth.api.listSCIMProviderConnections({
|
|
712
|
-
headers,
|
|
713
|
-
});
|
|
714
|
-
expect(
|
|
715
|
-
listBefore.providers?.some((p) => p.providerId === "my-provider"),
|
|
716
|
-
).toBe(true);
|
|
717
|
-
|
|
718
|
-
const deleteRes = await auth.api.deleteSCIMProviderConnection({
|
|
719
|
-
body: { providerId: "my-provider" },
|
|
720
|
-
headers,
|
|
721
|
-
});
|
|
722
|
-
expect(deleteRes).toMatchObject({ success: true });
|
|
723
|
-
|
|
724
|
-
const listAfter = await auth.api.listSCIMProviderConnections({ headers });
|
|
725
|
-
expect(
|
|
726
|
-
listAfter.providers?.some((p) => p.providerId === "my-provider"),
|
|
727
|
-
).toBe(false);
|
|
728
|
-
|
|
729
|
-
await expect(
|
|
730
|
-
auth.api.getSCIMUser({
|
|
731
|
-
params: { userId: "any" },
|
|
732
|
-
headers: {
|
|
733
|
-
Authorization: `Bearer ${scimToken}`,
|
|
734
|
-
},
|
|
735
|
-
}),
|
|
736
|
-
).rejects.toThrow();
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
it("should return 403 when provider belongs to another org", async () => {
|
|
740
|
-
const { auth, getAuthCookieHeaders, registerOrganization } =
|
|
741
|
-
createTestInstance();
|
|
742
|
-
|
|
743
|
-
const [headers1, headers2] = await Promise.all([
|
|
744
|
-
getAuthCookieHeaders(policyUserA),
|
|
745
|
-
getAuthCookieHeaders(policyUserB),
|
|
746
|
-
]);
|
|
747
|
-
|
|
748
|
-
const [org1, _org2] = await Promise.all([
|
|
749
|
-
registerOrganization("del-policy-org-1", headers1),
|
|
750
|
-
registerOrganization("del-policy-org-2", headers2),
|
|
751
|
-
]);
|
|
752
|
-
|
|
753
|
-
await auth.api.generateSCIMToken({
|
|
754
|
-
body: { providerId: "other-org-del", organizationId: org1?.id },
|
|
755
|
-
headers: headers1,
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
await expect(
|
|
759
|
-
auth.api.deleteSCIMProviderConnection({
|
|
760
|
-
body: { providerId: "other-org-del" },
|
|
761
|
-
headers: headers2,
|
|
762
|
-
}),
|
|
763
|
-
).rejects.toMatchObject({
|
|
764
|
-
status: "FORBIDDEN",
|
|
765
|
-
message:
|
|
766
|
-
"You must be a member of the organization to access this provider",
|
|
767
|
-
});
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
it("should return 404 for unknown providerId", async () => {
|
|
771
|
-
const { auth, getAuthCookieHeaders } = createTestInstance();
|
|
772
|
-
const headers = await getAuthCookieHeaders();
|
|
773
|
-
|
|
774
|
-
await expect(
|
|
775
|
-
auth.api.deleteSCIMProviderConnection({
|
|
776
|
-
body: { providerId: "unknown" },
|
|
777
|
-
headers,
|
|
778
|
-
}),
|
|
779
|
-
).rejects.toMatchObject({
|
|
780
|
-
message: "SCIM provider not found",
|
|
781
|
-
});
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
it("should deny delete of non-org provider when user is not the owner", async () => {
|
|
785
|
-
const { auth, getAuthCookieHeaders } = createTestInstance({
|
|
786
|
-
providerOwnership: { enabled: true },
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
const [headersUserA, headersUserB] = await Promise.all([
|
|
790
|
-
getAuthCookieHeaders(policyUserA),
|
|
791
|
-
getAuthCookieHeaders(policyUserB),
|
|
792
|
-
]);
|
|
793
|
-
|
|
794
|
-
await auth.api.generateSCIMToken({
|
|
795
|
-
body: { providerId: "user-a-delete-provider" },
|
|
796
|
-
headers: headersUserA,
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
await expect(
|
|
800
|
-
auth.api.deleteSCIMProviderConnection({
|
|
801
|
-
body: { providerId: "user-a-delete-provider" },
|
|
802
|
-
headers: headersUserB,
|
|
803
|
-
}),
|
|
804
|
-
).rejects.toMatchObject({
|
|
805
|
-
status: "FORBIDDEN",
|
|
806
|
-
message: "You must be the owner to access this provider",
|
|
807
|
-
});
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
});
|