@maroonedsoftware/scim 0.1.0
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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/errors/scim.error.d.ts +42 -0
- package/dist/errors/scim.error.d.ts.map +1 -0
- package/dist/filter/filter.ast.d.ts +40 -0
- package/dist/filter/filter.ast.d.ts.map +1 -0
- package/dist/filter/filter.parser.d.ts +13 -0
- package/dist/filter/filter.parser.d.ts.map +1 -0
- package/dist/filter/filter.tokenizer.d.ts +14 -0
- package/dist/filter/filter.tokenizer.d.ts.map +1 -0
- package/dist/filter/index.d.ts +4 -0
- package/dist/filter/index.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2153 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/require.scim.scope.middleware.d.ts +11 -0
- package/dist/middleware/require.scim.scope.middleware.d.ts.map +1 -0
- package/dist/middleware/scim.content.type.middleware.d.ts +10 -0
- package/dist/middleware/scim.content.type.middleware.d.ts.map +1 -0
- package/dist/middleware/scim.error.middleware.d.ts +12 -0
- package/dist/middleware/scim.error.middleware.d.ts.map +1 -0
- package/dist/patch/index.d.ts +2 -0
- package/dist/patch/index.d.ts.map +1 -0
- package/dist/patch/patch.applier.d.ts +11 -0
- package/dist/patch/patch.applier.d.ts.map +1 -0
- package/dist/repositories/index.d.ts +4 -0
- package/dist/repositories/index.d.ts.map +1 -0
- package/dist/repositories/repository.types.d.ts +35 -0
- package/dist/repositories/repository.types.d.ts.map +1 -0
- package/dist/repositories/scim.group.repository.d.ts +23 -0
- package/dist/repositories/scim.group.repository.d.ts.map +1 -0
- package/dist/repositories/scim.user.repository.d.ts +34 -0
- package/dist/repositories/scim.user.repository.d.ts.map +1 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/scim.router.d.ts +46 -0
- package/dist/router/scim.router.d.ts.map +1 -0
- package/dist/schemas/enterprise.user.schema.d.ts +8 -0
- package/dist/schemas/enterprise.user.schema.d.ts.map +1 -0
- package/dist/schemas/group.schema.d.ts +8 -0
- package/dist/schemas/group.schema.d.ts.map +1 -0
- package/dist/schemas/index.d.ts +9 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/resource.type.schema.d.ts +21 -0
- package/dist/schemas/resource.type.schema.d.ts.map +1 -0
- package/dist/schemas/schema.types.d.ts +33 -0
- package/dist/schemas/schema.types.d.ts.map +1 -0
- package/dist/schemas/service.provider.config.schema.d.ts +53 -0
- package/dist/schemas/service.provider.config.schema.d.ts.map +1 -0
- package/dist/schemas/user.schema.d.ts +9 -0
- package/dist/schemas/user.schema.d.ts.map +1 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/scim.group.service.d.ts +59 -0
- package/dist/services/scim.group.service.d.ts.map +1 -0
- package/dist/services/scim.service.provider.service.d.ts +33 -0
- package/dist/services/scim.service.provider.service.d.ts.map +1 -0
- package/dist/services/scim.user.service.d.ts +62 -0
- package/dist/services/scim.user.service.d.ts.map +1 -0
- package/dist/types/list.response.d.ts +15 -0
- package/dist/types/list.response.d.ts.map +1 -0
- package/dist/types/patch.op.d.ts +25 -0
- package/dist/types/patch.op.d.ts.map +1 -0
- package/dist/types/scim.group.d.ts +14 -0
- package/dist/types/scim.group.d.ts.map +1 -0
- package/dist/types/scim.meta.d.ts +30 -0
- package/dist/types/scim.meta.d.ts.map +1 -0
- package/dist/types/scim.user.d.ts +75 -0
- package/dist/types/scim.user.d.ts.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2153 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/types/list.response.ts
|
|
5
|
+
var ListResponseSchema = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
|
|
6
|
+
|
|
7
|
+
// src/types/patch.op.ts
|
|
8
|
+
var PatchOpSchema = "urn:ietf:params:scim:api:messages:2.0:PatchOp";
|
|
9
|
+
|
|
10
|
+
// src/schemas/user.schema.ts
|
|
11
|
+
var UserSchemaId = "urn:ietf:params:scim:schemas:core:2.0:User";
|
|
12
|
+
var userSchema = {
|
|
13
|
+
id: UserSchemaId,
|
|
14
|
+
name: "User",
|
|
15
|
+
description: "User Account",
|
|
16
|
+
meta: {
|
|
17
|
+
resourceType: "Schema",
|
|
18
|
+
location: `/Schemas/${UserSchemaId}`
|
|
19
|
+
},
|
|
20
|
+
attributes: [
|
|
21
|
+
{
|
|
22
|
+
name: "userName",
|
|
23
|
+
type: "string",
|
|
24
|
+
multiValued: false,
|
|
25
|
+
description: "Unique identifier for the User, typically used by the user to directly authenticate.",
|
|
26
|
+
required: true,
|
|
27
|
+
caseExact: false,
|
|
28
|
+
mutability: "readWrite",
|
|
29
|
+
returned: "default",
|
|
30
|
+
uniqueness: "server"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "name",
|
|
34
|
+
type: "complex",
|
|
35
|
+
multiValued: false,
|
|
36
|
+
description: "The components of the user's real name.",
|
|
37
|
+
required: false,
|
|
38
|
+
mutability: "readWrite",
|
|
39
|
+
returned: "default",
|
|
40
|
+
uniqueness: "none",
|
|
41
|
+
subAttributes: [
|
|
42
|
+
{
|
|
43
|
+
name: "formatted",
|
|
44
|
+
type: "string",
|
|
45
|
+
multiValued: false,
|
|
46
|
+
description: "Full name formatted for display.",
|
|
47
|
+
required: false,
|
|
48
|
+
caseExact: false,
|
|
49
|
+
mutability: "readWrite",
|
|
50
|
+
returned: "default",
|
|
51
|
+
uniqueness: "none"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "familyName",
|
|
55
|
+
type: "string",
|
|
56
|
+
multiValued: false,
|
|
57
|
+
description: "Family name (last name).",
|
|
58
|
+
required: false,
|
|
59
|
+
caseExact: false,
|
|
60
|
+
mutability: "readWrite",
|
|
61
|
+
returned: "default",
|
|
62
|
+
uniqueness: "none"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "givenName",
|
|
66
|
+
type: "string",
|
|
67
|
+
multiValued: false,
|
|
68
|
+
description: "Given name (first name).",
|
|
69
|
+
required: false,
|
|
70
|
+
caseExact: false,
|
|
71
|
+
mutability: "readWrite",
|
|
72
|
+
returned: "default",
|
|
73
|
+
uniqueness: "none"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "middleName",
|
|
77
|
+
type: "string",
|
|
78
|
+
multiValued: false,
|
|
79
|
+
description: "Middle name.",
|
|
80
|
+
required: false,
|
|
81
|
+
caseExact: false,
|
|
82
|
+
mutability: "readWrite",
|
|
83
|
+
returned: "default",
|
|
84
|
+
uniqueness: "none"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "honorificPrefix",
|
|
88
|
+
type: "string",
|
|
89
|
+
multiValued: false,
|
|
90
|
+
description: "Honorific prefix (e.g., Ms., Dr.).",
|
|
91
|
+
required: false,
|
|
92
|
+
caseExact: false,
|
|
93
|
+
mutability: "readWrite",
|
|
94
|
+
returned: "default",
|
|
95
|
+
uniqueness: "none"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "honorificSuffix",
|
|
99
|
+
type: "string",
|
|
100
|
+
multiValued: false,
|
|
101
|
+
description: "Honorific suffix (e.g., Jr., III).",
|
|
102
|
+
required: false,
|
|
103
|
+
caseExact: false,
|
|
104
|
+
mutability: "readWrite",
|
|
105
|
+
returned: "default",
|
|
106
|
+
uniqueness: "none"
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "displayName",
|
|
112
|
+
type: "string",
|
|
113
|
+
multiValued: false,
|
|
114
|
+
description: "Name displayed to end-users.",
|
|
115
|
+
required: false,
|
|
116
|
+
caseExact: false,
|
|
117
|
+
mutability: "readWrite",
|
|
118
|
+
returned: "default",
|
|
119
|
+
uniqueness: "none"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "nickName",
|
|
123
|
+
type: "string",
|
|
124
|
+
multiValued: false,
|
|
125
|
+
description: "Casual name of the User.",
|
|
126
|
+
required: false,
|
|
127
|
+
caseExact: false,
|
|
128
|
+
mutability: "readWrite",
|
|
129
|
+
returned: "default",
|
|
130
|
+
uniqueness: "none"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "profileUrl",
|
|
134
|
+
type: "reference",
|
|
135
|
+
multiValued: false,
|
|
136
|
+
description: "URL of the User's online profile.",
|
|
137
|
+
required: false,
|
|
138
|
+
caseExact: false,
|
|
139
|
+
mutability: "readWrite",
|
|
140
|
+
returned: "default",
|
|
141
|
+
uniqueness: "none",
|
|
142
|
+
referenceTypes: [
|
|
143
|
+
"external"
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "title",
|
|
148
|
+
type: "string",
|
|
149
|
+
multiValued: false,
|
|
150
|
+
description: "Title of the User.",
|
|
151
|
+
required: false,
|
|
152
|
+
caseExact: false,
|
|
153
|
+
mutability: "readWrite",
|
|
154
|
+
returned: "default",
|
|
155
|
+
uniqueness: "none"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "userType",
|
|
159
|
+
type: "string",
|
|
160
|
+
multiValued: false,
|
|
161
|
+
description: "Used to identify the relationship between the organization and the user.",
|
|
162
|
+
required: false,
|
|
163
|
+
caseExact: false,
|
|
164
|
+
mutability: "readWrite",
|
|
165
|
+
returned: "default",
|
|
166
|
+
uniqueness: "none"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "preferredLanguage",
|
|
170
|
+
type: "string",
|
|
171
|
+
multiValued: false,
|
|
172
|
+
description: "Preferred language as per RFC 7231.",
|
|
173
|
+
required: false,
|
|
174
|
+
caseExact: false,
|
|
175
|
+
mutability: "readWrite",
|
|
176
|
+
returned: "default",
|
|
177
|
+
uniqueness: "none"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "locale",
|
|
181
|
+
type: "string",
|
|
182
|
+
multiValued: false,
|
|
183
|
+
description: "Default location of the User.",
|
|
184
|
+
required: false,
|
|
185
|
+
caseExact: false,
|
|
186
|
+
mutability: "readWrite",
|
|
187
|
+
returned: "default",
|
|
188
|
+
uniqueness: "none"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "timezone",
|
|
192
|
+
type: "string",
|
|
193
|
+
multiValued: false,
|
|
194
|
+
description: "Time zone in IANA format.",
|
|
195
|
+
required: false,
|
|
196
|
+
caseExact: false,
|
|
197
|
+
mutability: "readWrite",
|
|
198
|
+
returned: "default",
|
|
199
|
+
uniqueness: "none"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "active",
|
|
203
|
+
type: "boolean",
|
|
204
|
+
multiValued: false,
|
|
205
|
+
description: "Whether the user is active.",
|
|
206
|
+
required: false,
|
|
207
|
+
mutability: "readWrite",
|
|
208
|
+
returned: "default",
|
|
209
|
+
uniqueness: "none"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "password",
|
|
213
|
+
type: "string",
|
|
214
|
+
multiValued: false,
|
|
215
|
+
description: "Cleartext password (write-only).",
|
|
216
|
+
required: false,
|
|
217
|
+
caseExact: false,
|
|
218
|
+
mutability: "writeOnly",
|
|
219
|
+
returned: "never",
|
|
220
|
+
uniqueness: "none"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "emails",
|
|
224
|
+
type: "complex",
|
|
225
|
+
multiValued: true,
|
|
226
|
+
description: "Email addresses for the user.",
|
|
227
|
+
required: false,
|
|
228
|
+
mutability: "readWrite",
|
|
229
|
+
returned: "default",
|
|
230
|
+
uniqueness: "none",
|
|
231
|
+
subAttributes: multiValuedSubAttrs("email")
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "phoneNumbers",
|
|
235
|
+
type: "complex",
|
|
236
|
+
multiValued: true,
|
|
237
|
+
description: "Phone numbers for the user.",
|
|
238
|
+
required: false,
|
|
239
|
+
mutability: "readWrite",
|
|
240
|
+
returned: "default",
|
|
241
|
+
uniqueness: "none",
|
|
242
|
+
subAttributes: multiValuedSubAttrs("phone")
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "groups",
|
|
246
|
+
type: "complex",
|
|
247
|
+
multiValued: true,
|
|
248
|
+
description: "Groups the user is a member of (read-only).",
|
|
249
|
+
required: false,
|
|
250
|
+
mutability: "readOnly",
|
|
251
|
+
returned: "default",
|
|
252
|
+
uniqueness: "none",
|
|
253
|
+
subAttributes: [
|
|
254
|
+
{
|
|
255
|
+
name: "value",
|
|
256
|
+
type: "string",
|
|
257
|
+
multiValued: false,
|
|
258
|
+
description: "Identifier of the Group.",
|
|
259
|
+
required: false,
|
|
260
|
+
caseExact: true,
|
|
261
|
+
mutability: "readOnly",
|
|
262
|
+
returned: "default",
|
|
263
|
+
uniqueness: "none"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "$ref",
|
|
267
|
+
type: "reference",
|
|
268
|
+
multiValued: false,
|
|
269
|
+
description: "URI of the Group resource.",
|
|
270
|
+
required: false,
|
|
271
|
+
caseExact: true,
|
|
272
|
+
mutability: "readOnly",
|
|
273
|
+
returned: "default",
|
|
274
|
+
uniqueness: "none",
|
|
275
|
+
referenceTypes: [
|
|
276
|
+
"Group"
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "display",
|
|
281
|
+
type: "string",
|
|
282
|
+
multiValued: false,
|
|
283
|
+
description: "Human-readable Group name.",
|
|
284
|
+
required: false,
|
|
285
|
+
caseExact: false,
|
|
286
|
+
mutability: "readOnly",
|
|
287
|
+
returned: "default",
|
|
288
|
+
uniqueness: "none"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "type",
|
|
292
|
+
type: "string",
|
|
293
|
+
multiValued: false,
|
|
294
|
+
description: "Membership type.",
|
|
295
|
+
required: false,
|
|
296
|
+
caseExact: false,
|
|
297
|
+
mutability: "readOnly",
|
|
298
|
+
returned: "default",
|
|
299
|
+
uniqueness: "none",
|
|
300
|
+
canonicalValues: [
|
|
301
|
+
"direct",
|
|
302
|
+
"indirect"
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
};
|
|
309
|
+
function multiValuedSubAttrs(kind) {
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
name: "value",
|
|
313
|
+
type: "string",
|
|
314
|
+
multiValued: false,
|
|
315
|
+
description: `${kind} value.`,
|
|
316
|
+
required: false,
|
|
317
|
+
caseExact: false,
|
|
318
|
+
mutability: "readWrite",
|
|
319
|
+
returned: "default",
|
|
320
|
+
uniqueness: "none"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "display",
|
|
324
|
+
type: "string",
|
|
325
|
+
multiValued: false,
|
|
326
|
+
description: "Human-readable display value.",
|
|
327
|
+
required: false,
|
|
328
|
+
caseExact: false,
|
|
329
|
+
mutability: "readWrite",
|
|
330
|
+
returned: "default",
|
|
331
|
+
uniqueness: "none"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "type",
|
|
335
|
+
type: "string",
|
|
336
|
+
multiValued: false,
|
|
337
|
+
description: `Function of the ${kind} (e.g., work, home).`,
|
|
338
|
+
required: false,
|
|
339
|
+
caseExact: false,
|
|
340
|
+
mutability: "readWrite",
|
|
341
|
+
returned: "default",
|
|
342
|
+
uniqueness: "none"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "primary",
|
|
346
|
+
type: "boolean",
|
|
347
|
+
multiValued: false,
|
|
348
|
+
description: "Whether this is the primary value.",
|
|
349
|
+
required: false,
|
|
350
|
+
mutability: "readWrite",
|
|
351
|
+
returned: "default",
|
|
352
|
+
uniqueness: "none"
|
|
353
|
+
}
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
__name(multiValuedSubAttrs, "multiValuedSubAttrs");
|
|
357
|
+
|
|
358
|
+
// src/schemas/group.schema.ts
|
|
359
|
+
var GroupSchemaId = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
|
360
|
+
var groupSchema = {
|
|
361
|
+
id: GroupSchemaId,
|
|
362
|
+
name: "Group",
|
|
363
|
+
description: "Group",
|
|
364
|
+
meta: {
|
|
365
|
+
resourceType: "Schema",
|
|
366
|
+
location: `/Schemas/${GroupSchemaId}`
|
|
367
|
+
},
|
|
368
|
+
attributes: [
|
|
369
|
+
{
|
|
370
|
+
name: "displayName",
|
|
371
|
+
type: "string",
|
|
372
|
+
multiValued: false,
|
|
373
|
+
description: "Human-readable name for the Group.",
|
|
374
|
+
required: true,
|
|
375
|
+
caseExact: false,
|
|
376
|
+
mutability: "readWrite",
|
|
377
|
+
returned: "default",
|
|
378
|
+
uniqueness: "none"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "members",
|
|
382
|
+
type: "complex",
|
|
383
|
+
multiValued: true,
|
|
384
|
+
description: "List of Group members.",
|
|
385
|
+
required: false,
|
|
386
|
+
mutability: "readWrite",
|
|
387
|
+
returned: "default",
|
|
388
|
+
uniqueness: "none",
|
|
389
|
+
subAttributes: [
|
|
390
|
+
{
|
|
391
|
+
name: "value",
|
|
392
|
+
type: "string",
|
|
393
|
+
multiValued: false,
|
|
394
|
+
description: "Identifier of the member.",
|
|
395
|
+
required: false,
|
|
396
|
+
caseExact: true,
|
|
397
|
+
mutability: "immutable",
|
|
398
|
+
returned: "default",
|
|
399
|
+
uniqueness: "none"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: "$ref",
|
|
403
|
+
type: "reference",
|
|
404
|
+
multiValued: false,
|
|
405
|
+
description: "URI of the member resource.",
|
|
406
|
+
required: false,
|
|
407
|
+
caseExact: true,
|
|
408
|
+
mutability: "immutable",
|
|
409
|
+
returned: "default",
|
|
410
|
+
uniqueness: "none",
|
|
411
|
+
referenceTypes: [
|
|
412
|
+
"User",
|
|
413
|
+
"Group"
|
|
414
|
+
]
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: "display",
|
|
418
|
+
type: "string",
|
|
419
|
+
multiValued: false,
|
|
420
|
+
description: "Human-readable name of the member.",
|
|
421
|
+
required: false,
|
|
422
|
+
caseExact: false,
|
|
423
|
+
mutability: "immutable",
|
|
424
|
+
returned: "default",
|
|
425
|
+
uniqueness: "none"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: "type",
|
|
429
|
+
type: "string",
|
|
430
|
+
multiValued: false,
|
|
431
|
+
description: "Member type.",
|
|
432
|
+
required: false,
|
|
433
|
+
caseExact: false,
|
|
434
|
+
mutability: "immutable",
|
|
435
|
+
returned: "default",
|
|
436
|
+
uniqueness: "none",
|
|
437
|
+
canonicalValues: [
|
|
438
|
+
"User",
|
|
439
|
+
"Group"
|
|
440
|
+
]
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// src/schemas/enterprise.user.schema.ts
|
|
448
|
+
var EnterpriseUserSchemaId = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User";
|
|
449
|
+
var enterpriseUserSchema = {
|
|
450
|
+
id: EnterpriseUserSchemaId,
|
|
451
|
+
name: "EnterpriseUser",
|
|
452
|
+
description: "Enterprise User extension",
|
|
453
|
+
meta: {
|
|
454
|
+
resourceType: "Schema",
|
|
455
|
+
location: `/Schemas/${EnterpriseUserSchemaId}`
|
|
456
|
+
},
|
|
457
|
+
attributes: [
|
|
458
|
+
{
|
|
459
|
+
name: "employeeNumber",
|
|
460
|
+
type: "string",
|
|
461
|
+
multiValued: false,
|
|
462
|
+
description: "Employee number.",
|
|
463
|
+
required: false,
|
|
464
|
+
caseExact: false,
|
|
465
|
+
mutability: "readWrite",
|
|
466
|
+
returned: "default",
|
|
467
|
+
uniqueness: "none"
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "costCenter",
|
|
471
|
+
type: "string",
|
|
472
|
+
multiValued: false,
|
|
473
|
+
description: "Cost center.",
|
|
474
|
+
required: false,
|
|
475
|
+
caseExact: false,
|
|
476
|
+
mutability: "readWrite",
|
|
477
|
+
returned: "default",
|
|
478
|
+
uniqueness: "none"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "organization",
|
|
482
|
+
type: "string",
|
|
483
|
+
multiValued: false,
|
|
484
|
+
description: "Organization name.",
|
|
485
|
+
required: false,
|
|
486
|
+
caseExact: false,
|
|
487
|
+
mutability: "readWrite",
|
|
488
|
+
returned: "default",
|
|
489
|
+
uniqueness: "none"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "division",
|
|
493
|
+
type: "string",
|
|
494
|
+
multiValued: false,
|
|
495
|
+
description: "Division name.",
|
|
496
|
+
required: false,
|
|
497
|
+
caseExact: false,
|
|
498
|
+
mutability: "readWrite",
|
|
499
|
+
returned: "default",
|
|
500
|
+
uniqueness: "none"
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: "department",
|
|
504
|
+
type: "string",
|
|
505
|
+
multiValued: false,
|
|
506
|
+
description: "Department name.",
|
|
507
|
+
required: false,
|
|
508
|
+
caseExact: false,
|
|
509
|
+
mutability: "readWrite",
|
|
510
|
+
returned: "default",
|
|
511
|
+
uniqueness: "none"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "manager",
|
|
515
|
+
type: "complex",
|
|
516
|
+
multiValued: false,
|
|
517
|
+
description: "The manager of the user.",
|
|
518
|
+
required: false,
|
|
519
|
+
mutability: "readWrite",
|
|
520
|
+
returned: "default",
|
|
521
|
+
uniqueness: "none",
|
|
522
|
+
subAttributes: [
|
|
523
|
+
{
|
|
524
|
+
name: "value",
|
|
525
|
+
type: "string",
|
|
526
|
+
multiValued: false,
|
|
527
|
+
description: "Manager user id.",
|
|
528
|
+
required: false,
|
|
529
|
+
caseExact: true,
|
|
530
|
+
mutability: "readWrite",
|
|
531
|
+
returned: "default",
|
|
532
|
+
uniqueness: "none"
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: "$ref",
|
|
536
|
+
type: "reference",
|
|
537
|
+
multiValued: false,
|
|
538
|
+
description: "URI of the manager User resource.",
|
|
539
|
+
required: false,
|
|
540
|
+
caseExact: true,
|
|
541
|
+
mutability: "readWrite",
|
|
542
|
+
returned: "default",
|
|
543
|
+
uniqueness: "none",
|
|
544
|
+
referenceTypes: [
|
|
545
|
+
"User"
|
|
546
|
+
]
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "displayName",
|
|
550
|
+
type: "string",
|
|
551
|
+
multiValued: false,
|
|
552
|
+
description: "Manager display name.",
|
|
553
|
+
required: false,
|
|
554
|
+
caseExact: false,
|
|
555
|
+
mutability: "readOnly",
|
|
556
|
+
returned: "default",
|
|
557
|
+
uniqueness: "none"
|
|
558
|
+
}
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// src/schemas/resource.type.schema.ts
|
|
565
|
+
var ResourceTypeSchemaId = "urn:ietf:params:scim:schemas:core:2.0:ResourceType";
|
|
566
|
+
var userResourceType = {
|
|
567
|
+
schemas: [
|
|
568
|
+
ResourceTypeSchemaId
|
|
569
|
+
],
|
|
570
|
+
id: "User",
|
|
571
|
+
name: "User",
|
|
572
|
+
endpoint: "/Users",
|
|
573
|
+
description: "User Account",
|
|
574
|
+
schema: UserSchemaId,
|
|
575
|
+
schemaExtensions: [
|
|
576
|
+
{
|
|
577
|
+
schema: EnterpriseUserSchemaId,
|
|
578
|
+
required: false
|
|
579
|
+
}
|
|
580
|
+
],
|
|
581
|
+
meta: {
|
|
582
|
+
resourceType: "ResourceType",
|
|
583
|
+
location: "/ResourceTypes/User"
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
var groupResourceType = {
|
|
587
|
+
schemas: [
|
|
588
|
+
ResourceTypeSchemaId
|
|
589
|
+
],
|
|
590
|
+
id: "Group",
|
|
591
|
+
name: "Group",
|
|
592
|
+
endpoint: "/Groups",
|
|
593
|
+
description: "Group",
|
|
594
|
+
schema: GroupSchemaId,
|
|
595
|
+
meta: {
|
|
596
|
+
resourceType: "ResourceType",
|
|
597
|
+
location: "/ResourceTypes/Group"
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/schemas/service.provider.config.schema.ts
|
|
602
|
+
var ServiceProviderConfigSchemaId = "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig";
|
|
603
|
+
var defaultBearer = {
|
|
604
|
+
type: "oauthbearertoken",
|
|
605
|
+
name: "OAuth Bearer Token",
|
|
606
|
+
description: "Authentication scheme using the OAuth Bearer Token Standard.",
|
|
607
|
+
specUri: "https://www.rfc-editor.org/info/rfc6750",
|
|
608
|
+
primary: true
|
|
609
|
+
};
|
|
610
|
+
var buildServiceProviderConfig = /* @__PURE__ */ __name((options = {}) => ({
|
|
611
|
+
schemas: [
|
|
612
|
+
ServiceProviderConfigSchemaId
|
|
613
|
+
],
|
|
614
|
+
documentationUri: options.documentationUri,
|
|
615
|
+
patch: options.patch ?? {
|
|
616
|
+
supported: true
|
|
617
|
+
},
|
|
618
|
+
bulk: options.bulk ?? {
|
|
619
|
+
supported: false,
|
|
620
|
+
maxOperations: 0,
|
|
621
|
+
maxPayloadSize: 0
|
|
622
|
+
},
|
|
623
|
+
filter: options.filter ?? {
|
|
624
|
+
supported: true,
|
|
625
|
+
maxResults: 200
|
|
626
|
+
},
|
|
627
|
+
changePassword: options.changePassword ?? {
|
|
628
|
+
supported: false
|
|
629
|
+
},
|
|
630
|
+
sort: options.sort ?? {
|
|
631
|
+
supported: true
|
|
632
|
+
},
|
|
633
|
+
etag: options.etag ?? {
|
|
634
|
+
supported: false
|
|
635
|
+
},
|
|
636
|
+
authenticationSchemes: options.authenticationSchemes ?? [
|
|
637
|
+
defaultBearer
|
|
638
|
+
],
|
|
639
|
+
meta: {
|
|
640
|
+
resourceType: "ServiceProviderConfig",
|
|
641
|
+
location: "/ServiceProviderConfig"
|
|
642
|
+
}
|
|
643
|
+
}), "buildServiceProviderConfig");
|
|
644
|
+
|
|
645
|
+
// src/schemas/index.ts
|
|
646
|
+
var coreSchemas = [
|
|
647
|
+
userSchema,
|
|
648
|
+
groupSchema,
|
|
649
|
+
enterpriseUserSchema
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
// src/errors/scim.error.ts
|
|
653
|
+
import { HttpError } from "@maroonedsoftware/errors";
|
|
654
|
+
var ScimErrorSchema = "urn:ietf:params:scim:api:messages:2.0:Error";
|
|
655
|
+
var ScimError = class _ScimError extends HttpError {
|
|
656
|
+
static {
|
|
657
|
+
__name(this, "ScimError");
|
|
658
|
+
}
|
|
659
|
+
/** SCIM-specific error subtype, see RFC 7644 §3.12. */
|
|
660
|
+
scimType;
|
|
661
|
+
constructor(statusCode, scimType, message) {
|
|
662
|
+
super(statusCode, message);
|
|
663
|
+
this.scimType = scimType;
|
|
664
|
+
Object.setPrototypeOf(this, _ScimError.prototype);
|
|
665
|
+
}
|
|
666
|
+
/** Build the SCIM error JSON body for this error. */
|
|
667
|
+
toScimBody() {
|
|
668
|
+
return {
|
|
669
|
+
schemas: [
|
|
670
|
+
ScimErrorSchema
|
|
671
|
+
],
|
|
672
|
+
status: String(this.statusCode),
|
|
673
|
+
...this.scimType ? {
|
|
674
|
+
scimType: this.scimType
|
|
675
|
+
} : {},
|
|
676
|
+
detail: this.message
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
var IsScimError = /* @__PURE__ */ __name((error) => error instanceof ScimError, "IsScimError");
|
|
681
|
+
var scimError = /* @__PURE__ */ __name((statusCode, scimType, message) => new ScimError(statusCode, scimType, message), "scimError");
|
|
682
|
+
|
|
683
|
+
// src/filter/filter.tokenizer.ts
|
|
684
|
+
var COMPARISON_OPS = /* @__PURE__ */ new Set([
|
|
685
|
+
"eq",
|
|
686
|
+
"ne",
|
|
687
|
+
"co",
|
|
688
|
+
"sw",
|
|
689
|
+
"ew",
|
|
690
|
+
"gt",
|
|
691
|
+
"ge",
|
|
692
|
+
"lt",
|
|
693
|
+
"le",
|
|
694
|
+
"pr"
|
|
695
|
+
]);
|
|
696
|
+
var tokenizeScimFilter = /* @__PURE__ */ __name((input) => {
|
|
697
|
+
const tokens = [];
|
|
698
|
+
let i = 0;
|
|
699
|
+
while (i < input.length) {
|
|
700
|
+
const ch = input[i];
|
|
701
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
702
|
+
i += 1;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (ch === "(") {
|
|
706
|
+
tokens.push({
|
|
707
|
+
kind: "lparen",
|
|
708
|
+
value: "(",
|
|
709
|
+
start: i
|
|
710
|
+
});
|
|
711
|
+
i += 1;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (ch === ")") {
|
|
715
|
+
tokens.push({
|
|
716
|
+
kind: "rparen",
|
|
717
|
+
value: ")",
|
|
718
|
+
start: i
|
|
719
|
+
});
|
|
720
|
+
i += 1;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (ch === "[") {
|
|
724
|
+
tokens.push({
|
|
725
|
+
kind: "lbracket",
|
|
726
|
+
value: "[",
|
|
727
|
+
start: i
|
|
728
|
+
});
|
|
729
|
+
i += 1;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (ch === "]") {
|
|
733
|
+
tokens.push({
|
|
734
|
+
kind: "rbracket",
|
|
735
|
+
value: "]",
|
|
736
|
+
start: i
|
|
737
|
+
});
|
|
738
|
+
i += 1;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (ch === '"') {
|
|
742
|
+
const start = i;
|
|
743
|
+
i += 1;
|
|
744
|
+
let value = "";
|
|
745
|
+
while (i < input.length && input[i] !== '"') {
|
|
746
|
+
if (input[i] === "\\" && i + 1 < input.length) {
|
|
747
|
+
const next = input[i + 1];
|
|
748
|
+
if (next === '"' || next === "\\" || next === "/") {
|
|
749
|
+
value += next;
|
|
750
|
+
i += 2;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (next === "n") {
|
|
754
|
+
value += "\n";
|
|
755
|
+
i += 2;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (next === "t") {
|
|
759
|
+
value += " ";
|
|
760
|
+
i += 2;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (next === "r") {
|
|
764
|
+
value += "\r";
|
|
765
|
+
i += 2;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (next === "b") {
|
|
769
|
+
value += "\b";
|
|
770
|
+
i += 2;
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (next === "f") {
|
|
774
|
+
value += "\f";
|
|
775
|
+
i += 2;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
throw scimError(400, "invalidFilter", "Bad Request").withDetails({
|
|
779
|
+
position: i,
|
|
780
|
+
message: `Unsupported escape sequence \\${next}`
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
value += input[i];
|
|
784
|
+
i += 1;
|
|
785
|
+
}
|
|
786
|
+
if (i >= input.length) {
|
|
787
|
+
throw scimError(400, "invalidFilter", "Bad Request").withDetails({
|
|
788
|
+
position: start,
|
|
789
|
+
message: "Unterminated string literal"
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
i += 1;
|
|
793
|
+
tokens.push({
|
|
794
|
+
kind: "string",
|
|
795
|
+
value,
|
|
796
|
+
start
|
|
797
|
+
});
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
801
|
+
const start = i;
|
|
802
|
+
let raw = "";
|
|
803
|
+
if (ch === "-") {
|
|
804
|
+
raw += ch;
|
|
805
|
+
i += 1;
|
|
806
|
+
}
|
|
807
|
+
while (i < input.length && /[0-9]/.test(input[i])) {
|
|
808
|
+
raw += input[i];
|
|
809
|
+
i += 1;
|
|
810
|
+
}
|
|
811
|
+
if (input[i] === ".") {
|
|
812
|
+
raw += ".";
|
|
813
|
+
i += 1;
|
|
814
|
+
while (i < input.length && /[0-9]/.test(input[i])) {
|
|
815
|
+
raw += input[i];
|
|
816
|
+
i += 1;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (input[i] === "e" || input[i] === "E") {
|
|
820
|
+
raw += input[i];
|
|
821
|
+
i += 1;
|
|
822
|
+
if (input[i] === "+" || input[i] === "-") {
|
|
823
|
+
raw += input[i];
|
|
824
|
+
i += 1;
|
|
825
|
+
}
|
|
826
|
+
while (i < input.length && /[0-9]/.test(input[i])) {
|
|
827
|
+
raw += input[i];
|
|
828
|
+
i += 1;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const num = Number(raw);
|
|
832
|
+
if (Number.isNaN(num)) {
|
|
833
|
+
throw scimError(400, "invalidFilter", "Bad Request").withDetails({
|
|
834
|
+
position: start,
|
|
835
|
+
message: `Invalid numeric literal "${raw}"`
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
tokens.push({
|
|
839
|
+
kind: "number",
|
|
840
|
+
value: num,
|
|
841
|
+
start
|
|
842
|
+
});
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (isIdentifierStart(ch)) {
|
|
846
|
+
const start = i;
|
|
847
|
+
let raw = "";
|
|
848
|
+
while (i < input.length && isIdentifierPart(input[i])) {
|
|
849
|
+
raw += input[i];
|
|
850
|
+
i += 1;
|
|
851
|
+
}
|
|
852
|
+
const lower = raw.toLowerCase();
|
|
853
|
+
switch (lower) {
|
|
854
|
+
case "true":
|
|
855
|
+
tokens.push({
|
|
856
|
+
kind: "true",
|
|
857
|
+
value: true,
|
|
858
|
+
start
|
|
859
|
+
});
|
|
860
|
+
continue;
|
|
861
|
+
case "false":
|
|
862
|
+
tokens.push({
|
|
863
|
+
kind: "false",
|
|
864
|
+
value: false,
|
|
865
|
+
start
|
|
866
|
+
});
|
|
867
|
+
continue;
|
|
868
|
+
case "null":
|
|
869
|
+
tokens.push({
|
|
870
|
+
kind: "null",
|
|
871
|
+
value: null,
|
|
872
|
+
start
|
|
873
|
+
});
|
|
874
|
+
continue;
|
|
875
|
+
case "and":
|
|
876
|
+
case "or":
|
|
877
|
+
case "not":
|
|
878
|
+
tokens.push({
|
|
879
|
+
kind: lower,
|
|
880
|
+
value: lower,
|
|
881
|
+
start
|
|
882
|
+
});
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (COMPARISON_OPS.has(lower)) {
|
|
886
|
+
tokens.push({
|
|
887
|
+
kind: "op",
|
|
888
|
+
value: lower,
|
|
889
|
+
start
|
|
890
|
+
});
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
tokens.push({
|
|
894
|
+
kind: "identifier",
|
|
895
|
+
value: raw,
|
|
896
|
+
start
|
|
897
|
+
});
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
throw scimError(400, "invalidFilter", "Bad Request").withDetails({
|
|
901
|
+
position: i,
|
|
902
|
+
message: `Unexpected character "${ch}"`
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
return tokens;
|
|
906
|
+
}, "tokenizeScimFilter");
|
|
907
|
+
var isIdentifierStart = /* @__PURE__ */ __name((ch) => /[A-Za-z_$]/.test(ch), "isIdentifierStart");
|
|
908
|
+
var isIdentifierPart = /* @__PURE__ */ __name((ch) => /[A-Za-z0-9_.\-:$]/.test(ch), "isIdentifierPart");
|
|
909
|
+
|
|
910
|
+
// src/filter/filter.parser.ts
|
|
911
|
+
var parseScimFilter = /* @__PURE__ */ __name((input) => {
|
|
912
|
+
const trimmed = input.trim();
|
|
913
|
+
if (trimmed.length === 0) {
|
|
914
|
+
throw scimError(400, "invalidFilter", "Bad Request").withDetails({
|
|
915
|
+
message: "Filter is empty"
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
const tokens = tokenizeScimFilter(trimmed);
|
|
919
|
+
const parser = new Parser(tokens);
|
|
920
|
+
const node = parser.parseOr();
|
|
921
|
+
parser.expectEnd();
|
|
922
|
+
return node;
|
|
923
|
+
}, "parseScimFilter");
|
|
924
|
+
var Parser = class Parser2 {
|
|
925
|
+
static {
|
|
926
|
+
__name(this, "Parser");
|
|
927
|
+
}
|
|
928
|
+
tokens;
|
|
929
|
+
pos = 0;
|
|
930
|
+
constructor(tokens) {
|
|
931
|
+
this.tokens = tokens;
|
|
932
|
+
}
|
|
933
|
+
parseOr() {
|
|
934
|
+
let left = this.parseAnd();
|
|
935
|
+
while (this.peek()?.kind === "or") {
|
|
936
|
+
this.consume();
|
|
937
|
+
const right = this.parseAnd();
|
|
938
|
+
left = {
|
|
939
|
+
kind: "logical",
|
|
940
|
+
operator: "or",
|
|
941
|
+
left,
|
|
942
|
+
right
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return left;
|
|
946
|
+
}
|
|
947
|
+
parseAnd() {
|
|
948
|
+
let left = this.parseUnary();
|
|
949
|
+
while (this.peek()?.kind === "and") {
|
|
950
|
+
this.consume();
|
|
951
|
+
const right = this.parseUnary();
|
|
952
|
+
left = {
|
|
953
|
+
kind: "logical",
|
|
954
|
+
operator: "and",
|
|
955
|
+
left,
|
|
956
|
+
right
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
return left;
|
|
960
|
+
}
|
|
961
|
+
parseUnary() {
|
|
962
|
+
const token = this.peek();
|
|
963
|
+
if (token?.kind === "not") {
|
|
964
|
+
this.consume();
|
|
965
|
+
this.expect("lparen", 'Expected "(" after "not"');
|
|
966
|
+
const inner = this.parseOr();
|
|
967
|
+
this.expect("rparen", 'Expected ")" to close "not(...)"');
|
|
968
|
+
return {
|
|
969
|
+
kind: "not",
|
|
970
|
+
filter: inner
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
if (token?.kind === "lparen") {
|
|
974
|
+
this.consume();
|
|
975
|
+
const inner = this.parseOr();
|
|
976
|
+
this.expect("rparen", 'Expected ")"');
|
|
977
|
+
return inner;
|
|
978
|
+
}
|
|
979
|
+
return this.parseComparisonOrValuePath();
|
|
980
|
+
}
|
|
981
|
+
parseComparisonOrValuePath() {
|
|
982
|
+
const token = this.peek();
|
|
983
|
+
if (!token || token.kind !== "identifier") {
|
|
984
|
+
throw this.error(token, "Expected attribute path");
|
|
985
|
+
}
|
|
986
|
+
this.consume();
|
|
987
|
+
const attribute = String(token.value);
|
|
988
|
+
const next = this.peek();
|
|
989
|
+
if (next?.kind === "lbracket") {
|
|
990
|
+
this.consume();
|
|
991
|
+
const inner = this.parseOr();
|
|
992
|
+
this.expect("rbracket", 'Expected "]" to close value-path filter');
|
|
993
|
+
return {
|
|
994
|
+
kind: "valuePath",
|
|
995
|
+
attribute,
|
|
996
|
+
filter: inner
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
if (!next || next.kind !== "op") {
|
|
1000
|
+
throw this.error(next, "Expected comparison operator");
|
|
1001
|
+
}
|
|
1002
|
+
this.consume();
|
|
1003
|
+
const operator = String(next.value);
|
|
1004
|
+
if (operator === "pr") {
|
|
1005
|
+
return {
|
|
1006
|
+
kind: "comparison",
|
|
1007
|
+
attribute,
|
|
1008
|
+
operator
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
const valueToken = this.peek();
|
|
1012
|
+
if (!valueToken) {
|
|
1013
|
+
throw this.error(valueToken, `Expected value after operator "${operator}"`);
|
|
1014
|
+
}
|
|
1015
|
+
this.consume();
|
|
1016
|
+
if (valueToken.kind === "string" || valueToken.kind === "number" || valueToken.kind === "true" || valueToken.kind === "false" || valueToken.kind === "null") {
|
|
1017
|
+
return {
|
|
1018
|
+
kind: "comparison",
|
|
1019
|
+
attribute,
|
|
1020
|
+
operator,
|
|
1021
|
+
value: valueToken.value
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
throw this.error(valueToken, `Expected literal value after operator "${operator}"`);
|
|
1025
|
+
}
|
|
1026
|
+
expectEnd() {
|
|
1027
|
+
const remaining = this.peek();
|
|
1028
|
+
if (remaining) {
|
|
1029
|
+
throw this.error(remaining, "Unexpected trailing tokens");
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
peek() {
|
|
1033
|
+
return this.tokens[this.pos];
|
|
1034
|
+
}
|
|
1035
|
+
consume() {
|
|
1036
|
+
const token = this.tokens[this.pos];
|
|
1037
|
+
this.pos += 1;
|
|
1038
|
+
return token;
|
|
1039
|
+
}
|
|
1040
|
+
expect(kind, message) {
|
|
1041
|
+
const token = this.peek();
|
|
1042
|
+
if (!token || token.kind !== kind) {
|
|
1043
|
+
throw this.error(token, message);
|
|
1044
|
+
}
|
|
1045
|
+
this.consume();
|
|
1046
|
+
return token;
|
|
1047
|
+
}
|
|
1048
|
+
error(token, message) {
|
|
1049
|
+
return scimError(400, "invalidFilter", "Bad Request").withDetails({
|
|
1050
|
+
message,
|
|
1051
|
+
position: token?.start ?? -1
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
// src/patch/patch.applier.ts
|
|
1057
|
+
var applyScimPatch = /* @__PURE__ */ __name((resource, ops) => {
|
|
1058
|
+
let next = structuredClone(resource);
|
|
1059
|
+
for (const op of ops) {
|
|
1060
|
+
next = applyOne(next, op);
|
|
1061
|
+
}
|
|
1062
|
+
return next;
|
|
1063
|
+
}, "applyScimPatch");
|
|
1064
|
+
var applyOne = /* @__PURE__ */ __name((resource, op) => {
|
|
1065
|
+
const kind = normaliseOpKind(op.op);
|
|
1066
|
+
if (op.path === void 0 || op.path === "") {
|
|
1067
|
+
if (kind === "remove") {
|
|
1068
|
+
throw scimError(400, "noTarget", "Bad Request").withDetails({
|
|
1069
|
+
message: '"remove" requires a path'
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
if (typeof op.value !== "object" || op.value === null || Array.isArray(op.value)) {
|
|
1073
|
+
throw scimError(400, "invalidValue", "Bad Request").withDetails({
|
|
1074
|
+
message: "Pathless add/replace requires an object value"
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return mergeObject(resource, op.value, kind);
|
|
1078
|
+
}
|
|
1079
|
+
const path = parsePatchPath(op.path);
|
|
1080
|
+
return applyAtPath(resource, path, kind, op.value);
|
|
1081
|
+
}, "applyOne");
|
|
1082
|
+
var normaliseOpKind = /* @__PURE__ */ __name((op) => {
|
|
1083
|
+
const lower = String(op).toLowerCase();
|
|
1084
|
+
if (lower === "add" || lower === "replace" || lower === "remove") return lower;
|
|
1085
|
+
throw scimError(400, "invalidSyntax", "Bad Request").withDetails({
|
|
1086
|
+
message: `Unknown PATCH op "${op}"`
|
|
1087
|
+
});
|
|
1088
|
+
}, "normaliseOpKind");
|
|
1089
|
+
var parsePatchPath = /* @__PURE__ */ __name((raw) => {
|
|
1090
|
+
const bracketStart = raw.indexOf("[");
|
|
1091
|
+
if (bracketStart === -1) {
|
|
1092
|
+
return {
|
|
1093
|
+
segments: raw.split(".").filter(Boolean)
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
const bracketEnd = findMatchingBracket(raw, bracketStart);
|
|
1097
|
+
if (bracketEnd === -1) {
|
|
1098
|
+
throw scimError(400, "invalidPath", "Bad Request").withDetails({
|
|
1099
|
+
message: "Unterminated value-path filter"
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
const head = raw.slice(0, bracketStart);
|
|
1103
|
+
const filterSrc = raw.slice(bracketStart + 1, bracketEnd);
|
|
1104
|
+
const tail = raw.slice(bracketEnd + 1);
|
|
1105
|
+
const segments = head.split(".").filter(Boolean);
|
|
1106
|
+
if (segments.length === 0) {
|
|
1107
|
+
throw scimError(400, "invalidPath", "Bad Request").withDetails({
|
|
1108
|
+
message: "Value-path filter requires a target attribute"
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
const filter = parseScimFilter(filterSrc);
|
|
1112
|
+
let filterSubAttr;
|
|
1113
|
+
if (tail.length > 0) {
|
|
1114
|
+
if (!tail.startsWith(".")) {
|
|
1115
|
+
throw scimError(400, "invalidPath", "Bad Request").withDetails({
|
|
1116
|
+
message: 'Expected "." after value-path filter'
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
filterSubAttr = tail.slice(1);
|
|
1120
|
+
if (filterSubAttr.length === 0 || filterSubAttr.includes("[")) {
|
|
1121
|
+
throw scimError(400, "invalidPath", "Bad Request").withDetails({
|
|
1122
|
+
message: "Invalid sub-attribute after value-path filter"
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
segments,
|
|
1128
|
+
filter,
|
|
1129
|
+
filterSubAttr
|
|
1130
|
+
};
|
|
1131
|
+
}, "parsePatchPath");
|
|
1132
|
+
var findMatchingBracket = /* @__PURE__ */ __name((input, openIndex) => {
|
|
1133
|
+
let depth = 0;
|
|
1134
|
+
let inString = false;
|
|
1135
|
+
for (let i = openIndex; i < input.length; i += 1) {
|
|
1136
|
+
const ch = input[i];
|
|
1137
|
+
if (inString) {
|
|
1138
|
+
if (ch === "\\") {
|
|
1139
|
+
i += 1;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if (ch === '"') inString = false;
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
if (ch === '"') {
|
|
1146
|
+
inString = true;
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (ch === "[") depth += 1;
|
|
1150
|
+
else if (ch === "]") {
|
|
1151
|
+
depth -= 1;
|
|
1152
|
+
if (depth === 0) return i;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return -1;
|
|
1156
|
+
}, "findMatchingBracket");
|
|
1157
|
+
var applyAtPath = /* @__PURE__ */ __name((resource, path, kind, value) => {
|
|
1158
|
+
const [head, ...rest] = path.segments;
|
|
1159
|
+
if (!head) {
|
|
1160
|
+
throw scimError(400, "invalidPath", "Bad Request").withDetails({
|
|
1161
|
+
message: "Empty PATCH path"
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
if (path.filter && rest.length === 0) {
|
|
1165
|
+
return applyFilteredOp(resource, head, path.filter, path.filterSubAttr, kind, value);
|
|
1166
|
+
}
|
|
1167
|
+
if (rest.length === 0) {
|
|
1168
|
+
return applySimpleOp(resource, head, kind, value);
|
|
1169
|
+
}
|
|
1170
|
+
const child = resource[head];
|
|
1171
|
+
const childObj = isPlainObject(child) ? {
|
|
1172
|
+
...child
|
|
1173
|
+
} : {};
|
|
1174
|
+
const updated = applyAtPath(childObj, {
|
|
1175
|
+
...path,
|
|
1176
|
+
segments: rest
|
|
1177
|
+
}, kind, value);
|
|
1178
|
+
return {
|
|
1179
|
+
...resource,
|
|
1180
|
+
[head]: updated
|
|
1181
|
+
};
|
|
1182
|
+
}, "applyAtPath");
|
|
1183
|
+
var applySimpleOp = /* @__PURE__ */ __name((resource, attr, kind, value) => {
|
|
1184
|
+
if (kind === "remove") {
|
|
1185
|
+
const next = {
|
|
1186
|
+
...resource
|
|
1187
|
+
};
|
|
1188
|
+
delete next[attr];
|
|
1189
|
+
return next;
|
|
1190
|
+
}
|
|
1191
|
+
if (kind === "replace") {
|
|
1192
|
+
return {
|
|
1193
|
+
...resource,
|
|
1194
|
+
[attr]: value
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
const existing = resource[attr];
|
|
1198
|
+
if (Array.isArray(existing) && Array.isArray(value)) {
|
|
1199
|
+
return {
|
|
1200
|
+
...resource,
|
|
1201
|
+
[attr]: [
|
|
1202
|
+
...existing,
|
|
1203
|
+
...value
|
|
1204
|
+
]
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
if (Array.isArray(existing) && value !== void 0) {
|
|
1208
|
+
return {
|
|
1209
|
+
...resource,
|
|
1210
|
+
[attr]: [
|
|
1211
|
+
...existing,
|
|
1212
|
+
value
|
|
1213
|
+
]
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
if (existing === void 0 && Array.isArray(value)) {
|
|
1217
|
+
return {
|
|
1218
|
+
...resource,
|
|
1219
|
+
[attr]: [
|
|
1220
|
+
...value
|
|
1221
|
+
]
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
if (isPlainObject(existing) && isPlainObject(value)) {
|
|
1225
|
+
return {
|
|
1226
|
+
...resource,
|
|
1227
|
+
[attr]: {
|
|
1228
|
+
...existing,
|
|
1229
|
+
...value
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
return {
|
|
1234
|
+
...resource,
|
|
1235
|
+
[attr]: value
|
|
1236
|
+
};
|
|
1237
|
+
}, "applySimpleOp");
|
|
1238
|
+
var applyFilteredOp = /* @__PURE__ */ __name((resource, attr, filter, subAttr, kind, value) => {
|
|
1239
|
+
const collection = resource[attr];
|
|
1240
|
+
if (!Array.isArray(collection)) {
|
|
1241
|
+
if (kind === "add") {
|
|
1242
|
+
return {
|
|
1243
|
+
...resource,
|
|
1244
|
+
[attr]: [
|
|
1245
|
+
{
|
|
1246
|
+
...value
|
|
1247
|
+
}
|
|
1248
|
+
]
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
throw scimError(400, "noTarget", "Bad Request").withDetails({
|
|
1252
|
+
message: `Attribute "${attr}" is not multi-valued`
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
const matched = [];
|
|
1256
|
+
collection.forEach((item, index) => {
|
|
1257
|
+
if (isPlainObject(item) && evaluateFilter(item, filter)) matched.push(index);
|
|
1258
|
+
});
|
|
1259
|
+
if (matched.length === 0 && kind !== "add") {
|
|
1260
|
+
throw scimError(400, "noTarget", "Bad Request").withDetails({
|
|
1261
|
+
message: `No items matched value-path filter on "${attr}"`
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
if (kind === "remove") {
|
|
1265
|
+
const next2 = collection.filter((_, idx) => !matched.includes(idx));
|
|
1266
|
+
return {
|
|
1267
|
+
...resource,
|
|
1268
|
+
[attr]: next2
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
if (kind === "add" && matched.length === 0) {
|
|
1272
|
+
const seed = isPlainObject(value) ? {
|
|
1273
|
+
...value
|
|
1274
|
+
} : {
|
|
1275
|
+
value
|
|
1276
|
+
};
|
|
1277
|
+
return {
|
|
1278
|
+
...resource,
|
|
1279
|
+
[attr]: [
|
|
1280
|
+
...collection,
|
|
1281
|
+
seed
|
|
1282
|
+
]
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
const next = collection.map((item, idx) => {
|
|
1286
|
+
if (!matched.includes(idx) || !isPlainObject(item)) return item;
|
|
1287
|
+
if (subAttr !== void 0) {
|
|
1288
|
+
if (kind === "replace" || kind === "add") {
|
|
1289
|
+
return {
|
|
1290
|
+
...item,
|
|
1291
|
+
[subAttr]: value
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (kind === "replace") {
|
|
1296
|
+
return isPlainObject(value) ? {
|
|
1297
|
+
...item,
|
|
1298
|
+
...value
|
|
1299
|
+
} : value;
|
|
1300
|
+
}
|
|
1301
|
+
return isPlainObject(value) ? {
|
|
1302
|
+
...item,
|
|
1303
|
+
...value
|
|
1304
|
+
} : item;
|
|
1305
|
+
});
|
|
1306
|
+
return {
|
|
1307
|
+
...resource,
|
|
1308
|
+
[attr]: next
|
|
1309
|
+
};
|
|
1310
|
+
}, "applyFilteredOp");
|
|
1311
|
+
var mergeObject = /* @__PURE__ */ __name((target, source, kind) => {
|
|
1312
|
+
if (kind === "replace") {
|
|
1313
|
+
return {
|
|
1314
|
+
...target,
|
|
1315
|
+
...source
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
const out = {
|
|
1319
|
+
...target
|
|
1320
|
+
};
|
|
1321
|
+
for (const [key, val] of Object.entries(source)) {
|
|
1322
|
+
const existing = out[key];
|
|
1323
|
+
if (Array.isArray(existing) && Array.isArray(val)) {
|
|
1324
|
+
out[key] = [
|
|
1325
|
+
...existing,
|
|
1326
|
+
...val
|
|
1327
|
+
];
|
|
1328
|
+
} else if (isPlainObject(existing) && isPlainObject(val)) {
|
|
1329
|
+
out[key] = mergeObject(existing, val, kind);
|
|
1330
|
+
} else {
|
|
1331
|
+
out[key] = val;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return out;
|
|
1335
|
+
}, "mergeObject");
|
|
1336
|
+
var evaluateFilter = /* @__PURE__ */ __name((item, filter) => {
|
|
1337
|
+
switch (filter.kind) {
|
|
1338
|
+
case "comparison":
|
|
1339
|
+
return evaluateComparison(item, filter.attribute, filter.operator, filter.value);
|
|
1340
|
+
case "logical":
|
|
1341
|
+
return filter.operator === "and" ? evaluateFilter(item, filter.left) && evaluateFilter(item, filter.right) : evaluateFilter(item, filter.left) || evaluateFilter(item, filter.right);
|
|
1342
|
+
case "not":
|
|
1343
|
+
return !evaluateFilter(item, filter.filter);
|
|
1344
|
+
case "valuePath": {
|
|
1345
|
+
const nested = item[filter.attribute];
|
|
1346
|
+
if (!Array.isArray(nested)) return false;
|
|
1347
|
+
return nested.some((child) => isPlainObject(child) && evaluateFilter(child, filter.filter));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}, "evaluateFilter");
|
|
1351
|
+
var evaluateComparison = /* @__PURE__ */ __name((item, attr, op, value) => {
|
|
1352
|
+
const segments = attr.split(".");
|
|
1353
|
+
let current = item;
|
|
1354
|
+
for (const seg of segments) {
|
|
1355
|
+
if (!isPlainObject(current)) return false;
|
|
1356
|
+
current = current[seg];
|
|
1357
|
+
}
|
|
1358
|
+
if (op === "pr") return current !== void 0 && current !== null && current !== "";
|
|
1359
|
+
if (current === void 0 || current === null) return false;
|
|
1360
|
+
switch (op) {
|
|
1361
|
+
case "eq":
|
|
1362
|
+
return current === value;
|
|
1363
|
+
case "ne":
|
|
1364
|
+
return current !== value;
|
|
1365
|
+
case "co":
|
|
1366
|
+
return typeof current === "string" && typeof value === "string" && current.toLowerCase().includes(value.toLowerCase());
|
|
1367
|
+
case "sw":
|
|
1368
|
+
return typeof current === "string" && typeof value === "string" && current.toLowerCase().startsWith(value.toLowerCase());
|
|
1369
|
+
case "ew":
|
|
1370
|
+
return typeof current === "string" && typeof value === "string" && current.toLowerCase().endsWith(value.toLowerCase());
|
|
1371
|
+
case "gt":
|
|
1372
|
+
return compareScalar(current, value) > 0;
|
|
1373
|
+
case "ge":
|
|
1374
|
+
return compareScalar(current, value) >= 0;
|
|
1375
|
+
case "lt":
|
|
1376
|
+
return compareScalar(current, value) < 0;
|
|
1377
|
+
case "le":
|
|
1378
|
+
return compareScalar(current, value) <= 0;
|
|
1379
|
+
default:
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
}, "evaluateComparison");
|
|
1383
|
+
var compareScalar = /* @__PURE__ */ __name((a, b) => {
|
|
1384
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
1385
|
+
const sa = String(a);
|
|
1386
|
+
const sb = String(b);
|
|
1387
|
+
if (sa < sb) return -1;
|
|
1388
|
+
if (sa > sb) return 1;
|
|
1389
|
+
return 0;
|
|
1390
|
+
}, "compareScalar");
|
|
1391
|
+
var isPlainObject = /* @__PURE__ */ __name((value) => {
|
|
1392
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1393
|
+
}, "isPlainObject");
|
|
1394
|
+
|
|
1395
|
+
// src/repositories/scim.user.repository.ts
|
|
1396
|
+
var ScimUserRepository = class {
|
|
1397
|
+
static {
|
|
1398
|
+
__name(this, "ScimUserRepository");
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
// src/repositories/scim.group.repository.ts
|
|
1403
|
+
var ScimGroupRepository = class {
|
|
1404
|
+
static {
|
|
1405
|
+
__name(this, "ScimGroupRepository");
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
// src/services/scim.user.service.ts
|
|
1410
|
+
import { randomUUID } from "crypto";
|
|
1411
|
+
import { Injectable } from "injectkit";
|
|
1412
|
+
import { Logger } from "@maroonedsoftware/logger";
|
|
1413
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
1414
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1415
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1416
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1417
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1418
|
+
}
|
|
1419
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
1420
|
+
function _ts_metadata(k, v) {
|
|
1421
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1422
|
+
}
|
|
1423
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
1424
|
+
var ScimUserService = class {
|
|
1425
|
+
static {
|
|
1426
|
+
__name(this, "ScimUserService");
|
|
1427
|
+
}
|
|
1428
|
+
repository;
|
|
1429
|
+
logger;
|
|
1430
|
+
constructor(repository, logger) {
|
|
1431
|
+
this.repository = repository;
|
|
1432
|
+
this.logger = logger;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Fetch a single user by id.
|
|
1436
|
+
*
|
|
1437
|
+
* @throws {ScimError} 404 if no user exists with the given id.
|
|
1438
|
+
*/
|
|
1439
|
+
async get(id) {
|
|
1440
|
+
const user = await this.repository.findById(id);
|
|
1441
|
+
if (!user) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1442
|
+
message: `User "${id}" not found`
|
|
1443
|
+
});
|
|
1444
|
+
return user;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* List users matching a parsed SCIM query, returning the page plus the
|
|
1448
|
+
* total number of matches (for the `totalResults` envelope field).
|
|
1449
|
+
*/
|
|
1450
|
+
async list(query) {
|
|
1451
|
+
return this.repository.list(query);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Create a new user. Assigns a server-generated `id`, fills `meta.created`
|
|
1455
|
+
* and `meta.lastModified`, and ensures the resource's `schemas` includes the
|
|
1456
|
+
* core User URN (and the EnterpriseUser URN when the extension is present).
|
|
1457
|
+
*
|
|
1458
|
+
* @throws {ScimError} 400 `invalidValue` when `userName` is missing.
|
|
1459
|
+
* @throws {ScimError} 409 `uniqueness` when `userName` already exists.
|
|
1460
|
+
*/
|
|
1461
|
+
async create(payload) {
|
|
1462
|
+
if (!payload.userName) {
|
|
1463
|
+
throw scimError(400, "invalidValue", "Bad Request").withDetails({
|
|
1464
|
+
message: '"userName" is required'
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
const existing = await this.repository.findByUserName(payload.userName);
|
|
1468
|
+
if (existing) {
|
|
1469
|
+
throw scimError(409, "uniqueness", "Conflict").withDetails({
|
|
1470
|
+
message: `userName "${payload.userName}" already exists`
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1474
|
+
const id = payload.id ?? randomUUID();
|
|
1475
|
+
const user = {
|
|
1476
|
+
...payload,
|
|
1477
|
+
id,
|
|
1478
|
+
userName: payload.userName,
|
|
1479
|
+
schemas: this.normaliseSchemas(payload.schemas, payload),
|
|
1480
|
+
meta: {
|
|
1481
|
+
resourceType: "User",
|
|
1482
|
+
created: now,
|
|
1483
|
+
lastModified: now,
|
|
1484
|
+
location: `/Users/${id}`
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
this.logger.debug("scim: creating user", {
|
|
1488
|
+
id,
|
|
1489
|
+
userName: user.userName
|
|
1490
|
+
});
|
|
1491
|
+
return this.repository.create(user);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Replace an existing user wholesale (PUT semantics). Preserves the original
|
|
1495
|
+
* `id` and `meta.created`; updates `meta.lastModified`.
|
|
1496
|
+
*
|
|
1497
|
+
* @throws {ScimError} 404 when no user exists with the given id.
|
|
1498
|
+
* @throws {ScimError} 400 `invalidValue` when `userName` is missing.
|
|
1499
|
+
* @throws {ScimError} 409 `uniqueness` when changing `userName` would collide
|
|
1500
|
+
* with another existing user.
|
|
1501
|
+
*/
|
|
1502
|
+
async replace(id, payload) {
|
|
1503
|
+
const existing = await this.repository.findById(id);
|
|
1504
|
+
if (!existing) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1505
|
+
message: `User "${id}" not found`
|
|
1506
|
+
});
|
|
1507
|
+
if (!payload.userName) {
|
|
1508
|
+
throw scimError(400, "invalidValue", "Bad Request").withDetails({
|
|
1509
|
+
message: '"userName" is required'
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
if (payload.userName !== existing.userName) {
|
|
1513
|
+
const conflict = await this.repository.findByUserName(payload.userName);
|
|
1514
|
+
if (conflict && conflict.id !== id) {
|
|
1515
|
+
throw scimError(409, "uniqueness", "Conflict").withDetails({
|
|
1516
|
+
message: `userName "${payload.userName}" already exists`
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1521
|
+
const user = {
|
|
1522
|
+
...payload,
|
|
1523
|
+
id,
|
|
1524
|
+
userName: payload.userName,
|
|
1525
|
+
schemas: this.normaliseSchemas(payload.schemas, payload),
|
|
1526
|
+
meta: {
|
|
1527
|
+
...existing.meta,
|
|
1528
|
+
lastModified: now,
|
|
1529
|
+
location: existing.meta.location ?? `/Users/${id}`
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
return this.repository.replace(id, user);
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Apply a sequence of SCIM PATCH ops (RFC 7644 §3.5.2) and persist the
|
|
1536
|
+
* result via `replace`. Updates `meta.lastModified`.
|
|
1537
|
+
*
|
|
1538
|
+
* @throws {ScimError} 404 when no user exists with the given id.
|
|
1539
|
+
* @throws {ScimError} 400 propagated from {@link applyScimPatch} for invalid
|
|
1540
|
+
* paths, unknown ops, or no-target failures.
|
|
1541
|
+
*/
|
|
1542
|
+
async patch(id, ops) {
|
|
1543
|
+
const existing = await this.repository.findById(id);
|
|
1544
|
+
if (!existing) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1545
|
+
message: `User "${id}" not found`
|
|
1546
|
+
});
|
|
1547
|
+
const patched = applyScimPatch(existing, ops);
|
|
1548
|
+
patched.id = existing.id;
|
|
1549
|
+
patched.meta = {
|
|
1550
|
+
...existing.meta,
|
|
1551
|
+
lastModified: (/* @__PURE__ */ new Date()).toISOString()
|
|
1552
|
+
};
|
|
1553
|
+
return this.repository.replace(id, patched);
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Delete a user by id.
|
|
1557
|
+
*
|
|
1558
|
+
* @throws {ScimError} 404 when no user exists with the given id.
|
|
1559
|
+
*/
|
|
1560
|
+
async delete(id) {
|
|
1561
|
+
const existing = await this.repository.findById(id);
|
|
1562
|
+
if (!existing) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1563
|
+
message: `User "${id}" not found`
|
|
1564
|
+
});
|
|
1565
|
+
await this.repository.delete(id);
|
|
1566
|
+
}
|
|
1567
|
+
normaliseSchemas(provided, payload) {
|
|
1568
|
+
const schemas = new Set(provided ?? []);
|
|
1569
|
+
schemas.add(UserSchemaId);
|
|
1570
|
+
if (payload[EnterpriseUserSchemaId]) {
|
|
1571
|
+
schemas.add(EnterpriseUserSchemaId);
|
|
1572
|
+
}
|
|
1573
|
+
return Array.from(schemas);
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
ScimUserService = _ts_decorate([
|
|
1577
|
+
Injectable(),
|
|
1578
|
+
_ts_metadata("design:type", Function),
|
|
1579
|
+
_ts_metadata("design:paramtypes", [
|
|
1580
|
+
typeof ScimUserRepository === "undefined" ? Object : ScimUserRepository,
|
|
1581
|
+
typeof Logger === "undefined" ? Object : Logger
|
|
1582
|
+
])
|
|
1583
|
+
], ScimUserService);
|
|
1584
|
+
|
|
1585
|
+
// src/services/scim.group.service.ts
|
|
1586
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1587
|
+
import { Injectable as Injectable2 } from "injectkit";
|
|
1588
|
+
import { Logger as Logger2 } from "@maroonedsoftware/logger";
|
|
1589
|
+
function _ts_decorate2(decorators, target, key, desc) {
|
|
1590
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1591
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1592
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1593
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1594
|
+
}
|
|
1595
|
+
__name(_ts_decorate2, "_ts_decorate");
|
|
1596
|
+
function _ts_metadata2(k, v) {
|
|
1597
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1598
|
+
}
|
|
1599
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
1600
|
+
var ScimGroupService = class {
|
|
1601
|
+
static {
|
|
1602
|
+
__name(this, "ScimGroupService");
|
|
1603
|
+
}
|
|
1604
|
+
repository;
|
|
1605
|
+
logger;
|
|
1606
|
+
constructor(repository, logger) {
|
|
1607
|
+
this.repository = repository;
|
|
1608
|
+
this.logger = logger;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Fetch a single group by id.
|
|
1612
|
+
*
|
|
1613
|
+
* @throws {ScimError} 404 when no group exists with the given id.
|
|
1614
|
+
*/
|
|
1615
|
+
async get(id) {
|
|
1616
|
+
const group = await this.repository.findById(id);
|
|
1617
|
+
if (!group) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1618
|
+
message: `Group "${id}" not found`
|
|
1619
|
+
});
|
|
1620
|
+
return group;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* List groups matching a parsed SCIM query, returning the page plus the
|
|
1624
|
+
* total number of matches.
|
|
1625
|
+
*/
|
|
1626
|
+
async list(query) {
|
|
1627
|
+
return this.repository.list(query);
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Create a new group. Assigns a server-generated `id` and fills `meta`
|
|
1631
|
+
* timestamps.
|
|
1632
|
+
*
|
|
1633
|
+
* @throws {ScimError} 400 `invalidValue` when `displayName` is missing.
|
|
1634
|
+
* @throws {ScimError} 409 `uniqueness` when `displayName` already exists.
|
|
1635
|
+
*/
|
|
1636
|
+
async create(payload) {
|
|
1637
|
+
if (!payload.displayName) {
|
|
1638
|
+
throw scimError(400, "invalidValue", "Bad Request").withDetails({
|
|
1639
|
+
message: '"displayName" is required'
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
const existing = await this.repository.findByDisplayName(payload.displayName);
|
|
1643
|
+
if (existing) {
|
|
1644
|
+
throw scimError(409, "uniqueness", "Conflict").withDetails({
|
|
1645
|
+
message: `displayName "${payload.displayName}" already exists`
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1649
|
+
const id = payload.id ?? randomUUID2();
|
|
1650
|
+
const group = {
|
|
1651
|
+
...payload,
|
|
1652
|
+
id,
|
|
1653
|
+
displayName: payload.displayName,
|
|
1654
|
+
schemas: this.normaliseSchemas(payload.schemas),
|
|
1655
|
+
meta: {
|
|
1656
|
+
resourceType: "Group",
|
|
1657
|
+
created: now,
|
|
1658
|
+
lastModified: now,
|
|
1659
|
+
location: `/Groups/${id}`
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
this.logger.debug("scim: creating group", {
|
|
1663
|
+
id,
|
|
1664
|
+
displayName: group.displayName
|
|
1665
|
+
});
|
|
1666
|
+
return this.repository.create(group);
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Replace an existing group wholesale (PUT semantics). Preserves `id` and
|
|
1670
|
+
* `meta.created`; updates `meta.lastModified`.
|
|
1671
|
+
*
|
|
1672
|
+
* @throws {ScimError} 404 when no group exists with the given id.
|
|
1673
|
+
* @throws {ScimError} 400 `invalidValue` when `displayName` is missing.
|
|
1674
|
+
* @throws {ScimError} 409 `uniqueness` when changing `displayName` would collide.
|
|
1675
|
+
*/
|
|
1676
|
+
async replace(id, payload) {
|
|
1677
|
+
const existing = await this.repository.findById(id);
|
|
1678
|
+
if (!existing) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1679
|
+
message: `Group "${id}" not found`
|
|
1680
|
+
});
|
|
1681
|
+
if (!payload.displayName) {
|
|
1682
|
+
throw scimError(400, "invalidValue", "Bad Request").withDetails({
|
|
1683
|
+
message: '"displayName" is required'
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
if (payload.displayName !== existing.displayName) {
|
|
1687
|
+
const conflict = await this.repository.findByDisplayName(payload.displayName);
|
|
1688
|
+
if (conflict && conflict.id !== id) {
|
|
1689
|
+
throw scimError(409, "uniqueness", "Conflict").withDetails({
|
|
1690
|
+
message: `displayName "${payload.displayName}" already exists`
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1695
|
+
const group = {
|
|
1696
|
+
...payload,
|
|
1697
|
+
id,
|
|
1698
|
+
displayName: payload.displayName,
|
|
1699
|
+
schemas: this.normaliseSchemas(payload.schemas),
|
|
1700
|
+
meta: {
|
|
1701
|
+
...existing.meta,
|
|
1702
|
+
lastModified: now,
|
|
1703
|
+
location: existing.meta.location ?? `/Groups/${id}`
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
return this.repository.replace(id, group);
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Apply a sequence of SCIM PATCH ops and persist the result. Updates
|
|
1710
|
+
* `meta.lastModified`.
|
|
1711
|
+
*
|
|
1712
|
+
* @throws {ScimError} 404 when no group exists with the given id.
|
|
1713
|
+
* @throws {ScimError} 400 propagated from {@link applyScimPatch}.
|
|
1714
|
+
*/
|
|
1715
|
+
async patch(id, ops) {
|
|
1716
|
+
const existing = await this.repository.findById(id);
|
|
1717
|
+
if (!existing) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1718
|
+
message: `Group "${id}" not found`
|
|
1719
|
+
});
|
|
1720
|
+
const patched = applyScimPatch(existing, ops);
|
|
1721
|
+
patched.id = existing.id;
|
|
1722
|
+
patched.meta = {
|
|
1723
|
+
...existing.meta,
|
|
1724
|
+
lastModified: (/* @__PURE__ */ new Date()).toISOString()
|
|
1725
|
+
};
|
|
1726
|
+
return this.repository.replace(id, patched);
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Delete a group by id.
|
|
1730
|
+
*
|
|
1731
|
+
* @throws {ScimError} 404 when no group exists with the given id.
|
|
1732
|
+
*/
|
|
1733
|
+
async delete(id) {
|
|
1734
|
+
const existing = await this.repository.findById(id);
|
|
1735
|
+
if (!existing) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1736
|
+
message: `Group "${id}" not found`
|
|
1737
|
+
});
|
|
1738
|
+
await this.repository.delete(id);
|
|
1739
|
+
}
|
|
1740
|
+
normaliseSchemas(provided) {
|
|
1741
|
+
const schemas = new Set(provided ?? []);
|
|
1742
|
+
schemas.add(GroupSchemaId);
|
|
1743
|
+
return Array.from(schemas);
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
ScimGroupService = _ts_decorate2([
|
|
1747
|
+
Injectable2(),
|
|
1748
|
+
_ts_metadata2("design:type", Function),
|
|
1749
|
+
_ts_metadata2("design:paramtypes", [
|
|
1750
|
+
typeof ScimGroupRepository === "undefined" ? Object : ScimGroupRepository,
|
|
1751
|
+
typeof Logger2 === "undefined" ? Object : Logger2
|
|
1752
|
+
])
|
|
1753
|
+
], ScimGroupService);
|
|
1754
|
+
|
|
1755
|
+
// src/services/scim.service.provider.service.ts
|
|
1756
|
+
import { Injectable as Injectable3 } from "injectkit";
|
|
1757
|
+
function _ts_decorate3(decorators, target, key, desc) {
|
|
1758
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1759
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1760
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1761
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1762
|
+
}
|
|
1763
|
+
__name(_ts_decorate3, "_ts_decorate");
|
|
1764
|
+
function _ts_metadata3(k, v) {
|
|
1765
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1766
|
+
}
|
|
1767
|
+
__name(_ts_metadata3, "_ts_metadata");
|
|
1768
|
+
var ScimServiceProviderService = class {
|
|
1769
|
+
static {
|
|
1770
|
+
__name(this, "ScimServiceProviderService");
|
|
1771
|
+
}
|
|
1772
|
+
config;
|
|
1773
|
+
schemas;
|
|
1774
|
+
resourceTypes;
|
|
1775
|
+
constructor(options = {}) {
|
|
1776
|
+
this.config = buildServiceProviderConfig(options);
|
|
1777
|
+
this.schemas = coreSchemas;
|
|
1778
|
+
this.resourceTypes = [
|
|
1779
|
+
userResourceType,
|
|
1780
|
+
groupResourceType
|
|
1781
|
+
];
|
|
1782
|
+
}
|
|
1783
|
+
/** The materialised `/ServiceProviderConfig` response. */
|
|
1784
|
+
getServiceProviderConfig() {
|
|
1785
|
+
return this.config;
|
|
1786
|
+
}
|
|
1787
|
+
/** Every schema known to this server, served from `/Schemas`. */
|
|
1788
|
+
listSchemas() {
|
|
1789
|
+
return this.schemas;
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Look up a single schema by its URN.
|
|
1793
|
+
*
|
|
1794
|
+
* @throws {ScimError} 404 if the URN is not registered.
|
|
1795
|
+
*/
|
|
1796
|
+
getSchema(id) {
|
|
1797
|
+
const found = this.schemas.find((s) => s.id === id);
|
|
1798
|
+
if (!found) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1799
|
+
message: `Schema "${id}" not found`
|
|
1800
|
+
});
|
|
1801
|
+
return found;
|
|
1802
|
+
}
|
|
1803
|
+
/** Every resource type known to this server, served from `/ResourceTypes`. */
|
|
1804
|
+
listResourceTypes() {
|
|
1805
|
+
return this.resourceTypes;
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Look up a single resource type by id (e.g. `User`, `Group`).
|
|
1809
|
+
*
|
|
1810
|
+
* @throws {ScimError} 404 if the id is not registered.
|
|
1811
|
+
*/
|
|
1812
|
+
getResourceType(id) {
|
|
1813
|
+
const found = this.resourceTypes.find((rt) => rt.id === id);
|
|
1814
|
+
if (!found) throw scimError(404, void 0, "Not Found").withDetails({
|
|
1815
|
+
message: `ResourceType "${id}" not found`
|
|
1816
|
+
});
|
|
1817
|
+
return found;
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
ScimServiceProviderService = _ts_decorate3([
|
|
1821
|
+
Injectable3(),
|
|
1822
|
+
_ts_metadata3("design:type", Function),
|
|
1823
|
+
_ts_metadata3("design:paramtypes", [
|
|
1824
|
+
typeof ScimServiceProviderConfigOptions === "undefined" ? Object : ScimServiceProviderConfigOptions
|
|
1825
|
+
])
|
|
1826
|
+
], ScimServiceProviderService);
|
|
1827
|
+
|
|
1828
|
+
// src/middleware/scim.content.type.middleware.ts
|
|
1829
|
+
var SCIM_MEDIA_TYPE = "application/scim+json";
|
|
1830
|
+
var scimContentTypeMiddleware = /* @__PURE__ */ __name(() => {
|
|
1831
|
+
return async (ctx, next) => {
|
|
1832
|
+
const method = ctx.method.toUpperCase();
|
|
1833
|
+
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
|
1834
|
+
const contentType = (ctx.request.headers["content-type"] ?? "").toLowerCase();
|
|
1835
|
+
if (contentType && !contentType.includes(SCIM_MEDIA_TYPE) && !contentType.includes("application/json")) {
|
|
1836
|
+
throw scimError(415, void 0, "Unsupported Media Type").withDetails({
|
|
1837
|
+
message: `Content-Type must be ${SCIM_MEDIA_TYPE}`
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
await next();
|
|
1842
|
+
if (ctx.body !== void 0 && ctx.body !== null) {
|
|
1843
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
}, "scimContentTypeMiddleware");
|
|
1847
|
+
|
|
1848
|
+
// src/middleware/scim.error.middleware.ts
|
|
1849
|
+
import { IsHttpError, IsServerkitError } from "@maroonedsoftware/errors";
|
|
1850
|
+
var scimErrorMiddleware = /* @__PURE__ */ __name(() => {
|
|
1851
|
+
return async (ctx, next) => {
|
|
1852
|
+
try {
|
|
1853
|
+
await next();
|
|
1854
|
+
if (ctx.status === 404 && !ctx.body) {
|
|
1855
|
+
const status = 404;
|
|
1856
|
+
ctx.status = status;
|
|
1857
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1858
|
+
ctx.body = {
|
|
1859
|
+
schemas: [
|
|
1860
|
+
ScimErrorSchema
|
|
1861
|
+
],
|
|
1862
|
+
status: String(status),
|
|
1863
|
+
detail: `Not Found: ${ctx.URL.pathname}`
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
if (IsScimError(error)) {
|
|
1868
|
+
respondWithScimError(ctx, error);
|
|
1869
|
+
ctx.app.emit("error", error, ctx);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (IsHttpError(error)) {
|
|
1873
|
+
respondWithScimError(ctx, new ScimError(error.statusCode).withDetails(error.details ?? {}).withCause(error));
|
|
1874
|
+
ctx.app.emit("error", error, ctx);
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const status = 500;
|
|
1878
|
+
const detail = IsServerkitError(error) ? error.message : "Internal Server Error";
|
|
1879
|
+
ctx.status = status;
|
|
1880
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1881
|
+
ctx.body = {
|
|
1882
|
+
schemas: [
|
|
1883
|
+
ScimErrorSchema
|
|
1884
|
+
],
|
|
1885
|
+
status: String(status),
|
|
1886
|
+
detail
|
|
1887
|
+
};
|
|
1888
|
+
ctx.app.emit("error", error, ctx);
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
}, "scimErrorMiddleware");
|
|
1892
|
+
var respondWithScimError = /* @__PURE__ */ __name((ctx, error) => {
|
|
1893
|
+
ctx.status = error.statusCode;
|
|
1894
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1895
|
+
ctx.body = error.toScimBody();
|
|
1896
|
+
if (error.headers) {
|
|
1897
|
+
for (const [name, value] of Object.entries(error.headers)) {
|
|
1898
|
+
ctx.set(name, value);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}, "respondWithScimError");
|
|
1902
|
+
|
|
1903
|
+
// src/middleware/require.scim.scope.middleware.ts
|
|
1904
|
+
import { invalidAuthenticationSession } from "@maroonedsoftware/authentication";
|
|
1905
|
+
var requireScimScope = /* @__PURE__ */ __name((scope) => {
|
|
1906
|
+
return async (ctx, next) => {
|
|
1907
|
+
const session = ctx.authenticationSession;
|
|
1908
|
+
if (!session || session === invalidAuthenticationSession) {
|
|
1909
|
+
throw scimError(401, void 0, "Unauthorized").addHeader("WWW-Authenticate", 'Bearer error="invalid_token"').withDetails({
|
|
1910
|
+
message: "Missing or invalid bearer token"
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
const granted = session.claims?.scimScopes;
|
|
1914
|
+
if (!Array.isArray(granted) || !granted.includes("*") && !granted.includes(scope)) {
|
|
1915
|
+
throw scimError(403, "insufficientScope", "Forbidden").withDetails({
|
|
1916
|
+
message: `Scope "${scope}" required`
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
await next();
|
|
1920
|
+
};
|
|
1921
|
+
}, "requireScimScope");
|
|
1922
|
+
|
|
1923
|
+
// src/router/scim.router.ts
|
|
1924
|
+
import { bodyParserMiddleware, ServerKitRouter } from "@maroonedsoftware/koa";
|
|
1925
|
+
var createScimRouter = /* @__PURE__ */ __name((options) => {
|
|
1926
|
+
const router = ServerKitRouter();
|
|
1927
|
+
const guards = options.routeGuards ?? [];
|
|
1928
|
+
const json = bodyParserMiddleware([
|
|
1929
|
+
SCIM_MEDIA_TYPE,
|
|
1930
|
+
"application/json"
|
|
1931
|
+
]);
|
|
1932
|
+
const maxResults = options.maxResults ?? options.serviceProviderService.getServiceProviderConfig().filter.maxResults ?? 200;
|
|
1933
|
+
router.get("/ServiceProviderConfig", ...guards, async (ctx) => {
|
|
1934
|
+
ctx.body = options.serviceProviderService.getServiceProviderConfig();
|
|
1935
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1936
|
+
});
|
|
1937
|
+
router.get("/Schemas", ...guards, async (ctx) => {
|
|
1938
|
+
ctx.body = listEnvelope(options.serviceProviderService.listSchemas());
|
|
1939
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1940
|
+
});
|
|
1941
|
+
router.get("/Schemas/:id", ...guards, async (ctx) => {
|
|
1942
|
+
ctx.body = options.serviceProviderService.getSchema(ctx.params.id);
|
|
1943
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1944
|
+
});
|
|
1945
|
+
router.get("/ResourceTypes", ...guards, async (ctx) => {
|
|
1946
|
+
ctx.body = listEnvelope(options.serviceProviderService.listResourceTypes());
|
|
1947
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1948
|
+
});
|
|
1949
|
+
router.get("/ResourceTypes/:id", ...guards, async (ctx) => {
|
|
1950
|
+
ctx.body = options.serviceProviderService.getResourceType(ctx.params.id);
|
|
1951
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1952
|
+
});
|
|
1953
|
+
router.get("/Users", ...guards, async (ctx) => {
|
|
1954
|
+
const query = parseListQueryFromUrl(ctx.query, maxResults);
|
|
1955
|
+
const result = await options.userService.list(query);
|
|
1956
|
+
ctx.body = listEnvelope(result.resources, query, result.totalResults);
|
|
1957
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1958
|
+
});
|
|
1959
|
+
router.post("/Users/.search", ...guards, json, async (ctx) => {
|
|
1960
|
+
const requestBody = takeRequestBody(ctx);
|
|
1961
|
+
const query = parseListQueryFromBody(requestBody, maxResults);
|
|
1962
|
+
const result = await options.userService.list(query);
|
|
1963
|
+
ctx.body = listEnvelope(result.resources, query, result.totalResults);
|
|
1964
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1965
|
+
});
|
|
1966
|
+
router.post("/Users", ...guards, json, async (ctx) => {
|
|
1967
|
+
const payload = takeRequestBody(ctx);
|
|
1968
|
+
const created = await options.userService.create(payload);
|
|
1969
|
+
ctx.status = 201;
|
|
1970
|
+
ctx.body = created;
|
|
1971
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1972
|
+
if (created.meta.location) ctx.set("Location", created.meta.location);
|
|
1973
|
+
});
|
|
1974
|
+
router.get("/Users/:id", ...guards, async (ctx) => {
|
|
1975
|
+
ctx.body = await options.userService.get(ctx.params.id);
|
|
1976
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1977
|
+
});
|
|
1978
|
+
router.put("/Users/:id", ...guards, json, async (ctx) => {
|
|
1979
|
+
const payload = takeRequestBody(ctx);
|
|
1980
|
+
ctx.body = await options.userService.replace(ctx.params.id, payload);
|
|
1981
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1982
|
+
});
|
|
1983
|
+
router.patch("/Users/:id", ...guards, json, async (ctx) => {
|
|
1984
|
+
const requestBody = takeRequestBody(ctx);
|
|
1985
|
+
const ops = validatePatchRequest(requestBody);
|
|
1986
|
+
ctx.body = await options.userService.patch(ctx.params.id, ops);
|
|
1987
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1988
|
+
});
|
|
1989
|
+
router.delete("/Users/:id", ...guards, async (ctx) => {
|
|
1990
|
+
await options.userService.delete(ctx.params.id);
|
|
1991
|
+
ctx.status = 204;
|
|
1992
|
+
});
|
|
1993
|
+
router.get("/Groups", ...guards, async (ctx) => {
|
|
1994
|
+
const query = parseListQueryFromUrl(ctx.query, maxResults);
|
|
1995
|
+
const result = await options.groupService.list(query);
|
|
1996
|
+
ctx.body = listEnvelope(result.resources, query, result.totalResults);
|
|
1997
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
1998
|
+
});
|
|
1999
|
+
router.post("/Groups/.search", ...guards, json, async (ctx) => {
|
|
2000
|
+
const requestBody = takeRequestBody(ctx);
|
|
2001
|
+
const query = parseListQueryFromBody(requestBody, maxResults);
|
|
2002
|
+
const result = await options.groupService.list(query);
|
|
2003
|
+
ctx.body = listEnvelope(result.resources, query, result.totalResults);
|
|
2004
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
2005
|
+
});
|
|
2006
|
+
router.post("/Groups", ...guards, json, async (ctx) => {
|
|
2007
|
+
const payload = takeRequestBody(ctx);
|
|
2008
|
+
const created = await options.groupService.create(payload);
|
|
2009
|
+
ctx.status = 201;
|
|
2010
|
+
ctx.body = created;
|
|
2011
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
2012
|
+
if (created.meta.location) ctx.set("Location", created.meta.location);
|
|
2013
|
+
});
|
|
2014
|
+
router.get("/Groups/:id", ...guards, async (ctx) => {
|
|
2015
|
+
ctx.body = await options.groupService.get(ctx.params.id);
|
|
2016
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
2017
|
+
});
|
|
2018
|
+
router.put("/Groups/:id", ...guards, json, async (ctx) => {
|
|
2019
|
+
const payload = takeRequestBody(ctx);
|
|
2020
|
+
ctx.body = await options.groupService.replace(ctx.params.id, payload);
|
|
2021
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
2022
|
+
});
|
|
2023
|
+
router.patch("/Groups/:id", ...guards, json, async (ctx) => {
|
|
2024
|
+
const requestBody = takeRequestBody(ctx);
|
|
2025
|
+
const ops = validatePatchRequest(requestBody);
|
|
2026
|
+
ctx.body = await options.groupService.patch(ctx.params.id, ops);
|
|
2027
|
+
ctx.type = SCIM_MEDIA_TYPE;
|
|
2028
|
+
});
|
|
2029
|
+
router.delete("/Groups/:id", ...guards, async (ctx) => {
|
|
2030
|
+
await options.groupService.delete(ctx.params.id);
|
|
2031
|
+
ctx.status = 204;
|
|
2032
|
+
});
|
|
2033
|
+
return router;
|
|
2034
|
+
}, "createScimRouter");
|
|
2035
|
+
var takeRequestBody = /* @__PURE__ */ __name((ctx) => {
|
|
2036
|
+
const body = ctx.body;
|
|
2037
|
+
ctx.body = void 0;
|
|
2038
|
+
return body;
|
|
2039
|
+
}, "takeRequestBody");
|
|
2040
|
+
var parseListQueryFromUrl = /* @__PURE__ */ __name((query, maxResults) => {
|
|
2041
|
+
const filterRaw = pickStringParam(query, "filter");
|
|
2042
|
+
return {
|
|
2043
|
+
filter: filterRaw ? parseScimFilter(filterRaw) : void 0,
|
|
2044
|
+
startIndex: parsePositiveInt(pickStringParam(query, "startIndex"), 1),
|
|
2045
|
+
count: clamp(parsePositiveInt(pickStringParam(query, "count"), maxResults), 0, maxResults),
|
|
2046
|
+
sortBy: pickStringParam(query, "sortBy"),
|
|
2047
|
+
sortOrder: parseSortOrder(pickStringParam(query, "sortOrder")),
|
|
2048
|
+
attributes: parseCsvParam(pickStringParam(query, "attributes")),
|
|
2049
|
+
excludedAttributes: parseCsvParam(pickStringParam(query, "excludedAttributes"))
|
|
2050
|
+
};
|
|
2051
|
+
}, "parseListQueryFromUrl");
|
|
2052
|
+
var parseListQueryFromBody = /* @__PURE__ */ __name((body, maxResults) => {
|
|
2053
|
+
if (!isPlainObject2(body)) {
|
|
2054
|
+
throw scimError(400, "invalidSyntax", "Bad Request").withDetails({
|
|
2055
|
+
message: "Search body must be a JSON object"
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
const filterRaw = typeof body.filter === "string" ? body.filter : void 0;
|
|
2059
|
+
return {
|
|
2060
|
+
filter: filterRaw ? parseScimFilter(filterRaw) : void 0,
|
|
2061
|
+
startIndex: typeof body.startIndex === "number" && body.startIndex > 0 ? Math.floor(body.startIndex) : 1,
|
|
2062
|
+
count: clamp(typeof body.count === "number" ? Math.floor(body.count) : maxResults, 0, maxResults),
|
|
2063
|
+
sortBy: typeof body.sortBy === "string" ? body.sortBy : void 0,
|
|
2064
|
+
sortOrder: parseSortOrder(typeof body.sortOrder === "string" ? body.sortOrder : void 0),
|
|
2065
|
+
attributes: Array.isArray(body.attributes) ? body.attributes.filter((a) => typeof a === "string") : void 0,
|
|
2066
|
+
excludedAttributes: Array.isArray(body.excludedAttributes) ? body.excludedAttributes.filter((a) => typeof a === "string") : void 0
|
|
2067
|
+
};
|
|
2068
|
+
}, "parseListQueryFromBody");
|
|
2069
|
+
var parseSortOrder = /* @__PURE__ */ __name((raw) => {
|
|
2070
|
+
if (raw === "ascending" || raw === "descending") return raw;
|
|
2071
|
+
return void 0;
|
|
2072
|
+
}, "parseSortOrder");
|
|
2073
|
+
var parsePositiveInt = /* @__PURE__ */ __name((raw, fallback) => {
|
|
2074
|
+
if (raw === void 0) return fallback;
|
|
2075
|
+
const n = Number(raw);
|
|
2076
|
+
if (!Number.isFinite(n) || n < 1) return fallback;
|
|
2077
|
+
return Math.floor(n);
|
|
2078
|
+
}, "parsePositiveInt");
|
|
2079
|
+
var clamp = /* @__PURE__ */ __name((value, min, max) => Math.max(min, Math.min(max, value)), "clamp");
|
|
2080
|
+
var parseCsvParam = /* @__PURE__ */ __name((raw) => {
|
|
2081
|
+
if (!raw) return void 0;
|
|
2082
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2083
|
+
return parts.length > 0 ? parts : void 0;
|
|
2084
|
+
}, "parseCsvParam");
|
|
2085
|
+
var pickStringParam = /* @__PURE__ */ __name((query, key) => {
|
|
2086
|
+
const value = query[key];
|
|
2087
|
+
if (typeof value === "string") return value;
|
|
2088
|
+
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
2089
|
+
return void 0;
|
|
2090
|
+
}, "pickStringParam");
|
|
2091
|
+
var validatePatchRequest = /* @__PURE__ */ __name((body) => {
|
|
2092
|
+
if (!isPlainObject2(body)) {
|
|
2093
|
+
throw scimError(400, "invalidSyntax", "Bad Request").withDetails({
|
|
2094
|
+
message: "PATCH request must be a JSON object"
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
if (!Array.isArray(body.schemas) || !body.schemas.includes(PatchOpSchema)) {
|
|
2098
|
+
throw scimError(400, "invalidSyntax", "Bad Request").withDetails({
|
|
2099
|
+
message: `PATCH request schemas must include "${PatchOpSchema}"`
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
if (!Array.isArray(body.Operations) || body.Operations.length === 0) {
|
|
2103
|
+
throw scimError(400, "invalidValue", "Bad Request").withDetails({
|
|
2104
|
+
message: '"Operations" must be a non-empty array'
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
return body.Operations;
|
|
2108
|
+
}, "validatePatchRequest");
|
|
2109
|
+
var listEnvelope = /* @__PURE__ */ __name((resources, query, total) => ({
|
|
2110
|
+
schemas: [
|
|
2111
|
+
ListResponseSchema
|
|
2112
|
+
],
|
|
2113
|
+
totalResults: total ?? resources.length,
|
|
2114
|
+
startIndex: query?.startIndex ?? 1,
|
|
2115
|
+
itemsPerPage: resources.length,
|
|
2116
|
+
Resources: resources
|
|
2117
|
+
}), "listEnvelope");
|
|
2118
|
+
var isPlainObject2 = /* @__PURE__ */ __name((value) => {
|
|
2119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2120
|
+
}, "isPlainObject");
|
|
2121
|
+
export {
|
|
2122
|
+
EnterpriseUserSchemaId,
|
|
2123
|
+
GroupSchemaId,
|
|
2124
|
+
IsScimError,
|
|
2125
|
+
ListResponseSchema,
|
|
2126
|
+
PatchOpSchema,
|
|
2127
|
+
ResourceTypeSchemaId,
|
|
2128
|
+
SCIM_MEDIA_TYPE,
|
|
2129
|
+
ScimError,
|
|
2130
|
+
ScimErrorSchema,
|
|
2131
|
+
ScimGroupRepository,
|
|
2132
|
+
ScimGroupService,
|
|
2133
|
+
ScimServiceProviderService,
|
|
2134
|
+
ScimUserRepository,
|
|
2135
|
+
ScimUserService,
|
|
2136
|
+
ServiceProviderConfigSchemaId,
|
|
2137
|
+
UserSchemaId,
|
|
2138
|
+
applyScimPatch,
|
|
2139
|
+
buildServiceProviderConfig,
|
|
2140
|
+
coreSchemas,
|
|
2141
|
+
createScimRouter,
|
|
2142
|
+
enterpriseUserSchema,
|
|
2143
|
+
groupResourceType,
|
|
2144
|
+
groupSchema,
|
|
2145
|
+
parseScimFilter,
|
|
2146
|
+
requireScimScope,
|
|
2147
|
+
scimContentTypeMiddleware,
|
|
2148
|
+
scimError,
|
|
2149
|
+
scimErrorMiddleware,
|
|
2150
|
+
userResourceType,
|
|
2151
|
+
userSchema
|
|
2152
|
+
};
|
|
2153
|
+
//# sourceMappingURL=index.js.map
|